0%

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 classes:

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.

Try it

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 below it. That is not carelessness so much as arithmetic: three places had to change, and two were missed. With eight attributes and six classes it stops being a question of whether this happens.

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 of attributes, then those lines are carrying no information, and they exist only because the language required them.

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 attributesGenuinely decided
__init__ assigning each oneWhich attributes exist
__repr__ listing themWhether the class is a value at all
__eq__ comparing themWhether points must be positive
Field order in those methodsWhat 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__:

  • Result stores three values and compares two.

  • Duration stores two values and shows one.

  • AttemptSummary is a snapshot 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 representations.