Skip to content

Dataclasses: less boilerplate, same data model

AdvancedLesson 14 of 148 min

Let @dataclass write __init__, __repr__ and __eq__ for you, give fields safe defaults with field(), and make objects hashable and sortable: frozen, order.

The last two lessons wrote special methods by hand: __init__ to store the fields, __repr__ to show them, __eq__ and __hash__ to compare them. For a class that mainly holds data, that's a lot of typing, and easy to get subtly wrong. @dataclass writes them for you, from a list of fields with type annotations:

from dataclasses import dataclass, field


@dataclass
class Parcel:
    to: str
    weight_kg: float
    fragile: bool = False
    labels: list[str] = field(default_factory=list)


p = Parcel("Oslo", 2.5)
print(p)                          # a useful repr, for free
print(p == Parcel("Oslo", 2.5))   # True: __eq__ compares the fields
Parcel(to='Oslo', weight_kg=2.5, fragile=False, labels=[])
True

It's still an ordinary class: you can add your own methods, and nothing checks the types at run time (the annotations are for you and your tools). A line needs an annotation to become a field: weight_kg = 0 without one is a plain class attribute.

Safe defaults with field()

Remember the mutable default trap? Dataclasses won't let you fall into it. labels: list[str] = [] stops with a ValueError the moment the class is defined. field(default_factory=list) calls list() for each new object instead, so no two parcels share a list. Fields with defaults must come after the ones without, like a function's parameters.

frozen=True: hashable, because it can't change

A plain dataclass compares by value, so, just like the Seat class in the last lesson, it gets __hash__ = None: it can't be a dict key. Ask for frozen=True and the fields can't be changed after the object is made, so Python can safely give it a hash built from them:

from dataclasses import dataclass


@dataclass(frozen=True)
class Colour:
    red: int
    green: int
    blue: int


teal = Colour(0, 128, 128)
names = {teal: "teal"}
print(names[Colour(0, 128, 128)])   # teal: equal colours, equal hashes
print(Colour.__hash__ is not None)  # True

Try teal.red = 255 after that, and you get a FrozenInstanceError.

order=True: sorting for free

order=True adds <, <=, > and >=. Objects compare as if they were tuples of their fields, in the order the fields are written: the first field decides, and only a tie moves on to the next one. That makes sorted(), min() and max() work.

More in the Python docs: dataclasses, field(), mutable default values and frozen instances.

Your turn

Write a dataclass Version with three int fields, major, minor and patch (patch defaults to 0). Make it frozen, so versions can be dict keys, and ordered, so that the starting code’s sorted() and max() work: 2.1.0 is newer than 2.0.7, which is newer than 1.9.4.

Done →