Dataclasses

Python · Practical · 10 min

Most classes you write hold data; dataclasses remove the boilerplate and the bugs that come with it.

The @dataclass decorator reads the class's annotated attributes and generates __init__, __repr__ and __eq__ for you. Fields without defaults must come before fields with defaults, as in a normal signature.

Mutable defaults need care: tags: list[str] = [] is rejected with a ValueError, because it would be shared. Use field(default_factory=list) to create a fresh list per instance.

Useful options on the decorator:

  • frozen=True makes instances immutable (assignment raises FrozenInstanceError) and hashable, so they can be dict keys or set members
  • order=True adds <, <= etc., comparing fields in declaration order like a tuple
  • slots=True (3.10+) generates __slots__, saving memory and speeding attribute access
  • kw_only=True (3.10+) makes every field keyword-only

Add validation or derived fields in __post_init__, which the generated __init__ calls at the end. dataclasses.asdict(obj) converts to a dict (recursively), and dataclasses.replace(obj, field=new) returns a modified copy — the idiomatic way to "change" a frozen instance. Python 3.13 also added the generic copy.replace() that works with dataclasses.

Annotations are required to declare a field but are not enforced at runtime; Point("a", "b") is accepted. If you need validation and parsing of untrusted input (e.g. JSON from an API), a library such as Pydantic or attrs builds on the same idea.

Compared to NamedTuple, dataclasses are mutable by default, not iterable or indexable, and easier to extend with methods.

Example

from dataclasses import dataclass, field, asdict, replace

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

@dataclass
class Order:
    id: int
    items: list[str] = field(default_factory=list)
    total: float = 0.0

    def __post_init__(self):
        if self.total < 0:
            raise ValueError("total must be >= 0")

o = Order(1, ["tea"], 3.5)
print(o)                  # Order(id=1, items=['tea'], total=3.5)
print(o == Order(1, ["tea"], 3.5))   # True
print(asdict(o))          # {'id': 1, 'items': ['tea'], 'total': 3.5}

v = Version(3, 12)
print(sorted([Version(3, 13), v, Version(2)]))
print(replace(v, minor=13), {v: "supported"}[v])

Remember

  • @dataclass writes __init__, __repr__ and __eq__ from annotated fields
  • Use field(default_factory=list) for mutable defaults
  • frozen=True gives immutable, hashable value objects; replace() makes changed copies

Watch out for

  • Assuming type annotations are validated at runtime.

Check yourself

  1. Why does `@dataclass class C: xs: list = []` fail?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/dataclasses.html