Chapter 12 · practice
Errors as Part of Program Design
Raise a Useful Built-in Error
Chapter 5 used raise ValueError to refuse a question with no answer, and moved on. This chapter takes errors seriously, because in a program made of collaborating
Every
Refusing is not failing
There are three honest answers a method can give when it cannot do what was asked:
A wrong answer is not an error. It is an ordinary outcome with an ordinary 0 is right.
Compare that with being asked for a question that does not exist. There is no number that means “there was no such question”, and inventing one is how programs end up reporting a score of -1 to a learner.
The three answers:
| Situation | Answer |
|---|---|
| An ordinary outcome, even an unwanted one | return a value |
| Absence is expected and the caller will check | return None |
| The caller asked for something impossible | raise |
The distinction is whether the caller could reasonably have expected this. An empty QuestionBank can be a normal staging state while questions are being assembled. Whether a runnable Quiz may be empty is a separate construction contract, made explicit in the chapter project. A quiz 99 in a two-question bank is a mistake in the calling code, and hiding it helps nobody.
When raise runs, the
Choosing the type
Python’s built-in exceptions are not decoration. Each one tells the caller a different thing, and picking the right one is most of the work:
| Raise | When |
|---|---|
ValueError | The right kind of thing, an unusable value. An empty prompt, a negative point total. |
TypeError | The wrong kind of thing entirely. A |
KeyError | A missing key in something key-shaped. |
IndexError | A position outside a sequence. |
LookupError | Either of the last two, when you do not want to promise which. |
quiz.question_at(9)
The caller now knows the difference between “you passed the wrong sort of thing” and “you asked for a position that is not there”, and can handle them differently if it wants to. The exact type check is deliberate: True and False behave like the integers 1 and 0 in some Python operations, but they are choices, not question positions, so this API rejects them.
quiz.question_at("first") is called with a string. Which error fits?
Messages that end the search
A message is read by someone who cannot see your code, in a
raise IndexError("bad index")
raise IndexError(f"no question at {index}; this quiz has {len(self._questions)}")
The second one ends the investigation. The first starts it.
Include the offending value. It costs one
What not to raise
Exception itself. raise Exception("something went wrong") gives the caller nothing to catch specifically, so their only options are catching everything or nothing.
assert for input checking. Assertions are for things you believe cannot happen, and Python can be run with them removed. A rule about a caller’s input is not an assumption; it is a promise, and promises need raise.
The exercise gives one class three methods that currently accept nonsense.
Task
QuestionBank currently accepts nonsense and returns nonsense. Give each
question_at(index) raises int, including True and False, and IndexError when it is outside the bank. The TypeError message names the offending type (such as str or bool); the IndexError message names the offending index.
add(prompt, answer, points) raises ValueError for an empty or whitespace-only prompt, or for points below zero, naming the negative points in that message. Validate before appending so a refusal does not change the bank. Zero points is allowed here: an ungraded survey question is a real thing.
points_for(index, response) returns the points for a correct response and 0 for an incorrect one. A wrong answer is an ordinary outcome, not an error, so nothing is raised for it.
find(prompt) returns the matching question, or None when there is none. Absence is expected here and the caller will check.
Three different answers to “I cannot give you what you asked for”, and choosing between them is the exercise.