0%

Dataclasses and Value Objects · capstone

Capstone project: Project: Model Result Records and Configuration

This chapter’s whole fits in one sentence: let Python write the code that your attribute already determines, and keep writing the code that expresses a decision.

The project is a small results system where every choice from the chapter appears once.

What you are building

QuizConfig    how a quiz runs         changeable, has defaults
Points        an amount               frozen value
Question      a question              frozen value, built several ways
ResultRecord  one finished result     frozen value
Gradebook     the collection          entity, changes as results arrive

Three frozen values, one changeable configuration, and one entity that exists in order to grow.

The decisions worth pausing on

Why ResultRecord is frozen. A finished result is a fact about something that already happened. Nothing should be able to edit last week’s score, and freezing says so in a way a comment cannot.

Why Gradebook is not. It exists to accumulate. Freezing it would mean rebuilding the whole gradebook on every result. eq=False also preserves entity identity: two separate gradebooks do not become the same merely because their current records match.

Why Gradebook holds a list built per object. Lesson 4 and Lesson 8 both showed the same bug arriving by different doors: a default and a mutable class attribute. One Gradebook per program is the common case, which is exactly why sharing a list between two of them would go unnoticed for a long time.

Why is ResultRecord frozen while Gradebook is not?

The methods that are not boilerplate

@dataclass writes the constructors, reprs, and comparisons. What is left is yours, and it is what the grader spends most of its time on:

  • Question.from_dict and Question.simple, two more ways in;

  • Question.normalize, the helper that belongs to questions;

  • Question.is_correct and points_for, the rules a question owns;

  • ResultRecord.percentage and passed_under, facts about a result;

  • Gradebook.record, best, and average, comparing normalized percentages so results with different totals stay meaningful.

Not one of those could have been generated, because each one is a decision somebody made.

Finishing

Count the lines you did not have to write. Then read the ones you did, and check that every one of them says something that could not have been worked out from the list of attributes above it.

Add two results with different totals, such as 9/100 and 8/10, and print the best result plus the average percentage. That final run is the proof that the gradebook models results rather than merely passing the supplied examples.

Task

Build the five classes, using the generated machinery for everything it covers.

Points is a frozen holding a positive amount, with plus(other) returning a new Points.

Question is a frozen, validated value holding non-empty prompt and answer and points defaulting to Points(1).

QuizConfig is a changeable dataclass: a required non-empty title, pass_mark defaulting to 0.6 and constrained above 0 and at most 1, and tags starting as a new empty per config.

ResultRecord is a frozen value holding a non-empty learner, non-negative total, and score from 0 through total, with percentage and a passed_under(config) .

Gradebook is a changeable entity holding a non-empty title and one internal _records list. Use eq=False. It gets record(result), records() returning a safe copy, best(), and average().

Complete the methods named in the starter:

  • Question.from_dict(record) reads prompt, answer, and a Points value from points; simple(prompt, answer) uses Points(1). Both are class methods constructing through cls. Reject a non-Points points with .

  • Question.normalize(text) is a static method that strips and lowercases text. Use it on both sides in is_correct. points_for returns the integer amount for a correct answer and 0 otherwise.

  • QuizConfig.practice(title) and final_exam(title) are class methods choosing pass marks of 0.5 and 0.8 respectively; tags still start empty.

  • ResultRecord.percentage is the fraction score / total, or 0.0 for a zero total. passed_under(config) includes a result exactly on the config’s pass mark.

  • Gradebook.record accepts only ResultRecord values, raising TypeError otherwise. best() compares percentages and returns a highest result, or None when empty. average() is the mean of the recorded percentages, or 0.0 when empty.

Use ValueError for the invalid field values described above. Keep the gradebook history out of constructor arguments and its generated repr with init=False, repr=False, as in Lesson 7. No class prints.