Errors as Part of Program Design · practice
Translate a Low-level Error
Some errors arrive from below with a message written for a different audience.
parse_score("eight")
invalid literal for int() with base 10: 'eight'. That message is about int(). Whoever called parse_score was thinking about a score, and now has to work out what int() has to do with anything.
Catching low, raising meaningful
The fix is to catch the low-level error and raise one that speaks the caller’s language:
parse_score("eight")
Now the message is about scores, which is what the
The pattern is worth naming because it applies far beyond int(). A function that reads a file raises “file not found”; the thing calling it wants “no saved quiz for that learner”. Same event, different vocabulary, and the caller is entitled to the second.
Keeping the cause
The version above leaves the relationship implicit, which you can inspect:
__cause__ is None, but the original int() failure is still present as implicit __context__, and Python normally includes both
raise ... from ... makes that relationship explicit:
Now the exception records the original explicitly in __cause__: your message is for whoever reads it, and the original underneath is for whoever has to fix it. In a real traceback Python prints both, joined by “The above exception was the direct cause of the following exception”. Use from to make intentional translation unambiguous, not because implicit context would vanish otherwise.
What does raise ValueError("...") from error add over raise ValueError("...")?
Translate at a boundary, not everywhere
Translation costs a try and a handler, so it is only worth it where the vocabulary actually changes.
Worth translating. A storage error becoming a domain error. A parsing error becoming “this quiz file is malformed”. Anything where the low-level message names a tool the caller has never heard of.
Not worth translating. Passing an error along inside one layer, where nothing changes but the wording. Re-raising ValueError as ValueError with the same information adds a frame and no meaning.
There is also a version that is actively harmful:
try:
return int(text)
except ValueError:
return 0
That is not translation, it is concealment. A malformed score becomes a real-looking zero, and the learner’s report says they scored nothing rather than that their file is broken. If you cannot say what a
Naked re-raising
Sometimes you want to react to an error and still let it through. A bare raise inside a handler re-raises what you caught, with its traceback intact:
Recording and handling are different decisions. This lets you do the first without pretending to have done the second.
Task
Three
parse_points(text) returns the points as a whole number. On unparseable text it raises
lookup_question(bank, prompt) returns the question stored under prompt. On a missing prompt it raises ValueError naming the prompt, again with the original KeyError as the cause. A missing question is a bad request, not a bad key, and the caller should not have to know the bank is a
parse_row(parts) builds a (prompt, points) pair from a two-item ["prompt", "3"]. A row with the wrong number of parts raises ValueError describing it. Points are parsed with parse_points, and that error passes straight through: it already speaks the right language, so re-wrapping it would add a frame and no meaning.
Finally, audited_points(text, log) appends "bad points: <text>" to the log and then lets the error continue unchanged. Use a bare raise.