Skip to content
← Back to explainers

Python explainer

What a Dataclass Writes for You

About 4 min to read. Allow extra time to work through examples.

Compare a hand-written class with the same class as a dataclass, and learn defaults, default factories, frozen records and sorting.

This article uses Python. You can make it your practice mode to browse related content across the site.

You keep points on a map as small objects. Written by hand, the class needs a constructor, a way to print itself, and a way to compare two points:

class Point:
    def __init__(self, x: int, y: int) -> None:
        self.x = x
        self.y = y

    def __repr__(self) -> str:
        return f"Point(x={self.x!r}, y={self.y!r})"

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Point):
            return NotImplemented
        return (self.x, self.y) == (other.x, other.y)

That is ten lines of code to hold two numbers, and each new field means editing all three methods. If you forget __eq__, Python raises no error. Its default == checks whether both sides are the same object, so Point(1, 2) == Point(1, 2) would be False.

Let the decorator write the methods

Here is the same class as a dataclass:

from dataclasses import dataclass


@dataclass
class Point:
    x: int
    y: int


p = Point(1, 2)
print(p)                 # Point(x=1, y=2)
print(p == Point(1, 2))  # True

@dataclass reads the annotated names in the class body. Each one is a field. From the fields, in the order you wrote them, the decorator writes __init__, __repr__ and __eq__. The generated __eq__ compares two objects field by field.

The annotations describe the fields to mypy. Python does not check them when the program runs, so Point("a", "b") builds a point that holds two strings. mypy reports that call. Code that reads outside data has to check the values itself, and later exercises in this track show how.

Give a field a default

Write the default after the annotation. Fields with a default come after fields without one, as with function parameters:

from dataclasses import dataclass


@dataclass
class Job:
    name: str
    retries: int = 3


print(Job("backup"))  # Job(name='backup', retries=3)

Use a factory for a list or a dict

Python creates a default value once, when it defines the class. If the default were a list, every object would share that one list. So the decorator refuses items: list[str] = [] with a ValueError. Use field(default_factory=list) instead:

from dataclasses import dataclass, field


@dataclass
class Order:
    items: list[str] = field(default_factory=list)


first = Order()
second = Order()
first.items.append("tea")
print(second.items)  # []

A default factory is a function that takes no arguments. The generated __init__ calls it for each new object that gets no value for the field, so every Order has its own list.

Freeze a record with frozen=True

With frozen=True, assigning to a field after construction raises dataclasses.FrozenInstanceError, and mypy reports the assignment too. To get a changed copy, call dataclasses.replace, which builds a new object:

from dataclasses import dataclass, replace


@dataclass(frozen=True)
class Point:
    x: int
    y: int


p = Point(1, 2)
moved = replace(p, x=5)
print(p, moved)  # Point(x=1, y=2) Point(x=5, y=2)
p.x = 5          # raises FrozenInstanceError

Freezing is shallow. A frozen record can hold a list, and code can still append to that list. A frozen dataclass can also go in a set, and a plain one cannot. The article Why Some Objects Can Go in a Set, later in this track, explains why.

Sort records with order=True

order=True adds <, <=, > and >=. They compare the fields in the order you declared them, the way Python compares tuples. Here they compare major first, and minor only when the majors are equal.

from dataclasses import dataclass


@dataclass(order=True)
class Version:
    major: int
    minor: int


print(sorted([Version(1, 10), Version(1, 2)]))
# [Version(major=1, minor=2), Version(major=1, minor=10)]

The next exercise uses order=True and frozen=True to sort version numbers.

Check your understanding You add the field tags: list[str] = [] to a dataclass, and importing the module fails with a ValueError. What should the line be? Show answer

tags: list[str] = field(default_factory=list). The decorator rejects a list default because every object that used it would share one list. With default_factory=list, the generated __init__ calls list() for each new object, so each one gets its own empty list.

Going further

  • dataclasses.fields(Point) returns the fields in order. Code that works with any dataclass walks them this way.
  • The decorator has other options, such as kw_only=True and slots=True. Later items in this track cover both.
  • The generated __eq__ returns NotImplemented unless both objects have exactly the same class. So a Point does not equal an object of a subclass, even one with the same field values.

The dataclasses documentation lists every option.

step 1 of 28

Modelling Data with Dataclasses

Up next Sort Version Numbers with a Dataclass