0%

Dataclasses and Value Objects · practice

Create a First Dataclass

Python can write the boilerplate from the previous lesson. Here is the whole of Result:

Try it

Five lines. The __init__, the __repr__, and the __eq__ all exist, and all three know about every attribute, because all three were generated from the same .

The bug from the previous lesson is not fixed here. It is unavailable. There is nowhere for the three places to disagree, because there is only one place.

The line at the top

from dataclasses import dataclass is the first import in this course, so it deserves a sentence.

Python ships with a large collection of ready-made tools, called the standard library. They are not available by default, because loading everything into every program would be wasteful and would fill your namespace with hundreds of names. An import line says which ones you want. This one asks for the dataclass tool from the dataclasses collection, and afterwards dataclass is an ordinary name you can use.

That is all you need for this chapter. Chapter 10 covers imports properly, including writing of your own, and it is a genuinely large subject. Treat this line as the way dataclasses are spelled until then.

What each line does

@dataclass
class Result:
    learner: str
    score: int
    total: int

@dataclass above a class is the same shape as @property above a . It hands the class to a tool that inspects it and adds methods.

Inside, learner: str is an annotation: the name of an attribute and the type it is expected to hold. You met annotations on in Chapter 2, where Python does not enforce them and tools use them for checking. The same is true here, with one addition: @dataclass reads these lines to find out which attributes exist. It is the only feature in this course where an annotation changes what the program does.

So the are still not enforced:

Try it

Python builds it without complaint. The annotation records intent and lets a type checker warn you. If a must genuinely be a number, that is a Chapter 5 job: validate it.

What is score: int doing inside a dataclass?

What you get, and what you do not

@dataclass generates __init__, __repr__, and __eq__. That is the default, and it is worth knowing exactly where it stops:

Try it

No __hash__, for the reason Chapter 7 gave: a dataclass is by default, so hashing it would be unsafe. Lesson 5 shows the setting that makes a dataclass and hashable together.

There is also no __str__, automatic validation, or ordering unless you ask.

Your own methods still belong there

A dataclass is an ordinary class. Everything you know still applies:

Try it

The generated __init__ calls __post_init__ after assigning the fields. That hook is where a dataclass keeps Chapter 5’s construction rules. Generated __init__, __repr__, and __eq__; hand-written validation, is_correct, and __str__. For these methods, a definition you write yourself is kept instead of a generated version.

This is the point of the feature. It removes the lines you were going to type anyway, and leaves the design decisions exactly where they were.

Task

Replace the hand-written machinery with generated machinery, keeping everything that was a real decision.

Result has already been converted and the import is already in place. Use it as the pattern for turning Duration and Question into dataclasses. Each loses its __init__, __repr__, and __eq__, and gains an annotated attribute line for each it holds. Keep the attribute names and their order exactly as they are, since that is what the constructor calls at the bottom depend on.

Keep every that was not generated: Duration.total_seconds, Question.is_correct, and Question.__str__. Restore Question’s construction rules in __post_init__: prompt and answer must be non-empty, and points must be greater than zero.

Do not add a __hash__. The next lessons explain why that is not simply an oversight.

The behavior and custom display should stay the same. The generated repr now includes field names, such as Question(prompt=..., answer=..., points=...), so that line will change.