0%

Errors as Part of Program Design · capstone

Capstone project: Project: Design Meaningful Quiz Errors

The chapter’s in one line: an error is part of what a promises, so it gets designed rather than improvised.

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 under one base. Each is caught somewhere different, which is the test a new type has to pass.

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

DecisionWhere
Refuse rather than invent a question_at, respond
Choose a built-in when one already fitsthe TypeError for a bad
Translate a low-level failureload_points, keeping the cause
Carry the values as attributesQuestionNotFound, InvalidResponse
Catch only at the boundaryrun_quiz, and nowhere else
Do not swallow what you cannot handlethe 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 family and the program that uses it.

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 ’s type is exactly 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".