Errors as Part of Program Design · practice
Create a Domain Exception
Every error so far has been a built-in. Built-ins are the right default, and there is one thing they cannot do.
The problem with borrowing a type
A quiz program raises int(), which raises ValueError for unparseable text. A caller wanting to handle the first but not the second has no way to tell them apart:
try:
run_quiz(quiz, responses)
except ValueError:
... # a blank response? a malformed number? something else entirely?
The only way to know is to read the message, and matching on message text is a habit that breaks the first time someone improves the wording.
The fix is a type of your own:
check(" ")
class QuizError(Exception) is the whole definition. It needs no body beyond a docstring, because what it carries is its identity: a caller can now write except QuizError and catch exactly the failures this program chose to signal, and nothing else.
A small family
One exception type says “this program refused”. A few say what kind of refusal:
QuestionNotFound inherits from QuizError, so except QuizError catches it. That is what Chapter 9’s substitutability buys here: a caller who cares about the distinction catches the specific one, and a caller who does not catches the base and gets both.
This is a small family. Two or three
Why does InvalidResponse inherit from QuizError rather than directly from Exception?
When not to invent one
A custom exception is worth defining when a caller will plausibly want to catch this and not something else. That is the whole test.
Do not define one where a built-in already says it exactly. A wrong type is a TypeError, everywhere, in every Python program ever written. InvalidTypeError teaches your callers a private word for a public idea.
Do not define one per raise site. EmptyPromptError, BlankAnswerError, and NegativePointsError are three names for “this question is not valid”, and nobody will ever catch them separately.
A useful sequence: start with the built-in, and promote to your own type the first time you find yourself needing to catch it apart from something else.
Where they live
An exception type is part of a errors.py at the top of the package, imported by the domain modules that raise them.
That is also why the capstone’s required layout has an errors.py. It is not a filing convention; it is the place a caller looks to find out what this program can refuse to do.
Task
Give the quiz program a small
Define QuizError(Exception) as the base, with InvalidResponse and QuestionNotFound under it. Three classes, each needing only a docstring.
Then use them in QuestionBank:
question_at(index)raisesQuestionNotFoundwhen theis outside the bank, and TypeErrorunless the index’s type is exactlyint.TrueandFalseare not positions. A wrong type is aTypeErrorin every Python program, so it stays one.respond(index, response)raisesInvalidResponsefor a blank response, and otherwise compares the response after stripping surrounding whitespace and lowercasing it. Return the points for a match, or0for a wrong answer.
Finally, run(bank, answers) returns a (score, refused) pair. Here refused is a QuizError once per answer and records each message with str(error), which is the point of having a base. It must not catch TypeError: a wrong type is a bug in the calling code, not a refusal this program is making.