Errors as Part of Program Design · capstone
Capstone project: Project: Design Meaningful Quiz Errors
The chapter’s
The project puts every decision together on one small program.
The design
QuizError the base: anything this program refuses
QuestionNotFound no question at that position
InvalidResponse the answer could not be used
QuizConfigError the quiz itself is malformed
Three
Built-ins keep the jobs they already do. A wrong type is a TypeError. That is not laziness; TypeError is understood by every Python programmer, and a private name for it would be worse.
The decisions the grader checks
| Decision | Where |
|---|---|
| Refuse rather than invent a | question_at, respond |
| Choose a built-in when one already fits | the TypeError for a bad |
| Translate a low-level failure | load_points, keeping the cause |
| Carry the values as attributes | QuestionNotFound, InvalidResponse |
| Catch only at the boundary | run_quiz, and nowhere else |
| Do not swallow what you cannot handle | the TypeError path |
Question.points_for raises, Attempt.record calls it, and run_quiz calls that. Why does only run_quiz have a try?
What good looks like when you finish
Read your run_quiz and ask what a learner sees for each kind of failure:
a wrong answer scores zero and is not mentioned;
a blank answer is skipped with a line saying which question;
a missing question is skipped with a line saying how many there are;
a malformed quiz stops the run, because there is nothing to continue with;
a bug in your own code crashes, with its
intact.
Five different outcomes, and not one of them is “the program printed a number that was quietly wrong”. That is what this chapter was for.
Task
Build the
QuizError(Exception) is the base. Under it: QuestionNotFound(index, available) and InvalidResponse(prompt, response), each keeping its values as attributes and passing a message up with super().__init__, and QuizConfigError, which needs no attributes.
Quiz.__init__ raises QuizConfigError when given no questions: a quiz with nothing in it cannot be run, so it should not be built.
Quiz.question_at(index) raises TypeError unless the int, including for True and False, and QuestionNotFound when it is outside the quiz.
Quiz.points_for(index, response) raises InvalidResponse for a blank response, compares the response after stripping surrounding whitespace and lowercasing it, returns the points for a correct one, and 0 for a wrong one. A wrong answer is not an error.
load_points(raw) translates int()'s failure into QuizError, keeping the original as the cause.
run_quiz(quiz, answers, log) is the only place that catches. It returns the score. Each QuizError is skipped and recorded as "<TypeName>: <message>". A TypeError is recorded the same way and then re-raised, because a bug in the calling code is not a refusal. Whatever happens, it appends "answered N" last, using finally. Here N counts answer pairs examined, including wrong answers, skipped refusals, and the pair that raises a TypeError. Pairs after that failure are not examined or counted; an empty answer sequence records "answered 0".