Chapter 8 · practice
Dataclasses and Value Objects
Recognize Repetitive Class Code
Here is the Points class from the end of Chapter 7, complete and correct:
class Points:
def __init__(self, amount):
if amount <= 0:
raise ValueError("points must be greater than zero")
self._amount = amount
@property
def amount(self):
return self._amount
def __repr__(self):
return f"Points({self.amount!r})"
def __eq__(self, other):
if not isinstance(other, Points):
return NotImplemented
return self.amount == other.amount
def __hash__(self):
return hash(self.amount)
def __lt__(self, other):
if not isinstance(other, Points):
return NotImplemented
return self.amount < other.amount
More than twenty lines to say “a Points is a positive, read-only amount.”
Some lines are predictable machinery: storing the declared field, displaying it, and comparing it. Positivity, read-only access, hashing, and ordering are separate design promises. A dataclass can generate the predictable part, but it will not decide those promises for you.
The same shape, twice
Look at what happens when a program has several small
class Points:
def __init__(self, amount):
self.amount = amount
def __repr__(self):
return f"Points({self.amount!r})"
def __eq__(self, other):
if not isinstance(other, Points):
return NotImplemented
return self.amount == other.amount
class Duration:
def __init__(self, seconds):
self.seconds = seconds
def __repr__(self):
return f"Duration({self.seconds!r})"
def __eq__(self, other):
if not isinstance(other, Duration):
return NotImplemented
return self.seconds == other.seconds
Read those two side by side. Change three names and they are the same class. Nothing in Duration was thought about; it was typed, or more likely copied and edited.
Why copied code goes wrong
The cost is not the typing. It is that copies drift.
Two results out of different totals, reported as equal. The repr agrees with the lie, because it leaves out total as well.
Someone added total to __init__ and did not update the two
What went wrong in the Result class above?
What “boilerplate” means
Code like this has a name: boilerplate. Not code that is bad, but code whose content is completely determined by something you already said. If a reader can predict every line from the
The right response to boilerplate is not to write it faster. It is to ask whether anything can generate it, so there is nothing to keep in step.
Not all of it is boilerplate
Be precise about which parts qualify, because the next lesson generates exactly these and no more:
| Determined by the attributes | Genuinely decided |
|---|---|
__init__ assigning each one | Which attributes exist |
__repr__ listing them | Whether the class is a value at all |
__eq__ comparing them | Whether points must be positive |
| Field order in those methods | What is_correct means |
The left column is what the default dataclass settings can write for you. The right column is your design, and no tool will decide it. Hashing, ordering, freezing, and validation require explicit choices covered later in the chapter.
The exercise asks you to find the drift in a small program before you learn the feature that prevents it. Knowing exactly what the machinery is for is what stops the next lesson from feeling like magic.
Task
Three classes here were written by hand, and each has drifted away from its own attributes.
Repair all three so that every attribute assigned in __init__ also appears in __repr__ and in __eq__:
Resultstores three values and compares two.Durationstores two values and shows one.AttemptSummaryis asnapshot that stores three values, and its __eq__is missing entirely.
Follow the repr format the classes already use: ClassName(value, value), with each value written as it would appear in code.
The program at the bottom prints pairs that should not be equal. When you are finished, the three comparison lines should report False. The last two lines should show every stored value in the