0%

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 they are not accidents. They are part of the interface.

Every you write makes two promises: what it returns when things go well, and what it does when they do not. The second one has been implicit until now.

Refusing is not failing

There are three honest answers a method can give when it cannot do what was asked:

Try it

A wrong answer is not an error. It is an ordinary outcome with an ordinary , and returning 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:

SituationAnswer
An ordinary outcome, even an unwanted onereturn a value
Absence is expected and the caller will checkreturn None
The caller asked for something impossibleraise

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 of 99 in a two-question bank is a mistake in the calling code, and hiding it helps nobody.

When raise runs, the stops immediately and the propagates to its caller until some matching boundary handles it. That makes validation order part of correctness: do failure-prone checks before appending or changing state, so a refused operation leaves the object as it was.

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:

RaiseWhen
ValueErrorThe right kind of thing, an unusable value. An empty prompt, a negative point total.
TypeErrorThe wrong kind of thing entirely. A where a question was expected.
KeyErrorA missing key in something key-shaped.
IndexErrorA position outside a sequence.
LookupErrorEither of the last two, when you do not want to promise which.
Try it
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 , possibly at speed. It should answer: what was expected, what arrived, and where.

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 and it is the single most useful thing a message can carry.

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 the right refusal.

question_at(index) raises when the ’s type is not exactly 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.