Errors as Part of Program Design · practice
Follow an Error Through Collaborating Objects
A raised
In a program made of one
Watching it travel
Now hand it a number where text was expected:
run_quiz(attempt, questions, [42])
The TypeError was raised in Question.points_for. Nothing in Attempt.answer or run_quiz mentions errors at all, and both were interrupted anyway. The
File "...", line 33, in run_quiz
attempt.answer(question, response)
File "...", line 27, in answer
self.score = self.score + question.points_for(response)
File "...", line 12, in points_for
raise TypeError(f"a response must be text, not {type(response).__name__}")
TypeError: a response must be text, not int
Read a traceback bottom-up. The last line is what went wrong. The frame above it is where. The frames above that are how you got there.
points_for raises, and neither answer nor run_quiz has a try. What happens to Attempt.answer?
Why this is worth having
The middle of the program said nothing about errors, and that is the point. Attempt.answer has one job. Making it also decide what to do about every possible failure in a question would bury its actual work under handling for problems it cannot solve.
A
Compare it with the alternative, which programs without exceptions have to use:
def answer(self, question, response):
earned, error = question.points_for(response)
if error is not None:
return None, error
self.score = self.score + earned
return earned, None
Every function returns a
What travels with it
An exception carries its type, its message, and the whole chain of frames it passed through. That chain is why an error raised four objects deep is still diagnosable at the top, and it is why swallowing an exception is expensive: catching it and continuing throws away the only record of what happened.
The partial-state problem
There is a real cost, and Chapter 5 has already prepared you for it. When an exception interrupts a method partway, whatever it had already done stays done:
A recorded response and no score for it. The list was appended to, then the exception arrived, and the object is now inconsistent.
The fix is the Chapter 5 rule, and it is why that rule mattered: do everything that can fail first, then change state. Ordering the two lines the other way makes this method safe.
The next lesson decides who catches, and where.
Task
Make Attempt.answer safe against an interruption. It currently records the response before asking the question for points, so an error leaves a response with no score behind it. Reorder it so that everything which can fail happens before any state changes.
Do not add a try to Attempt or to Question. Neither can fix a bad response, so neither should catch anything.
The supplied trace_of(action) helper walks error.__traceback__ and returns the