dataclasses

Java has records, Lombok's @Data, and an IDE that writes equals/hashCode for you. Python's answer is a decorator that reads your type annotations and generates the methods at class-creation time — not a template, not a base class with magic, just ordinary functions written into the class. Everything it does you could do by hand, which is exactly what makes it worth measuring.

@dataclassfield()frozen default_factory__eq__ / __hash__

What the decorator writes for you

Pick the number of fields and the options, and compare: on the left is what you type, on the right is the hand-written class that behaves identically. Both are real, complete Python — the line counts underneath are counted from the text, not estimated:

Same class, both ways

how many attributes the record holds
generates < <= > >= so instances sort
immutable — and therefore hashable, so it can go in a set or dict key

The ratio is not the whole story — and it moves in an interesting direction. Adding fields grows both columns by the same three lines, so more fields actually make the saving proportionally smaller. What multiplies it is the options: order=True costs one keyword on the left and four methods on the right. And the kind of code matters more than the count. Nothing in the right-hand column is interesting; all of it is what goes wrong when you add a seventh field, update __init__ and __repr__, and forget __eq__. The decorator cannot forget, because it re-derives everything from the annotations every time the class is defined.

Three of the options are worth knowing by name:

The one trap: mutable defaults

This is the same trap as a mutable default argument, in a new place, and it is the one thing about dataclasses you must know before you use them. A default value is evaluated once, when the class is created — so tags: list = [] would give every instance the same list. Watch three separate objects each append one tag:

Three instances, one list — or three?

Python actually protects you here: @dataclass raises ValueError: mutable default at class-definition time if it sees a list, dict or set literal as a default — one of the few places the language refuses to let you make the mistake. The left-hand behaviour above is what you would get if you wrote the class by hand, and it is exactly why field(default_factory=list) exists: the factory is called once per instance, so each object gets its own.

from dataclasses import dataclass, field

@dataclass(frozen=True, order=True)
class Point:
    x: float
    y: float
    label: str = "origin"                        # immutable default: fine
    tags: list = field(default_factory=list)     # mutable: needs a factory

Two more things you get for free once a class is a dataclass: dataclasses.replace(p, x=3) returns a copy with one field changed — the standard way to "modify" a frozen instance — and asdict(p) gives you a nested dict, which is how these become JSON. For validation and parsing on top of the same idea, Phase 6 uses pydantic, whose models are dataclasses that also check types at runtime.

⚠️ Traps & honesty: the hand-written column is a faithful but not byte-identical equivalent — the real generated __init__ is built with exec and carries proper defaults and __qualname__; the line counts compare like for like in intent, not in bytes · order=True compares fields as a tuple, so it only means what you want if declaration order happens to be your sort order · eq=True with frozen=False sets __hash__ = None, which is a deliberate safety feature and a common surprise · a dataclass gives you no runtime type checking whatsoever — the annotations are documentation to Python · inheritance between dataclasses puts base fields first, so a base with defaults forces defaults on every subclass field after it.
Takeaways: @dataclass reads the annotations and generates __init__, __repr__ and __eq__ as ordinary methods — the same code you would write, minus the chance of forgetting one when a field is added · frozen=True makes it immutable and hashable; a plain mutable dataclass is deliberately unhashable · order=True sorts by fields in declaration order, so declaration order is a decision · a mutable default is shared by every instance, which is why field(default_factory=list) exists — and why the decorator refuses a list literal outright · replace() and asdict() come along for free. Next: Counter, pathlib & itertools.

Second opinion (taught here — these corroborate): dataclasses docs · PEP 557 · pydantic.