Dataclasses Generate a Value Policy, Not Just Boilerplate
How defaults, equality, ordering, frozen instances, and hashing interact in generated dataclass methods.
@dataclass examines annotated fields and generates methods according to a set of flags. The defaults—init=True, repr=True, and eq=True—create a convenient value-like class, but they also establish contracts around equality and hashing.
from dataclasses import dataclass, field
@dataclass
class Job:
name: str
tags: list[str] = field(default_factory=list)
default_factory avoids sharing one mutable list between instances. The factory is called once when the class is decorated, and its result is copied into each new instance. This keeps construction cheap while still isolating the field values.
Equality requires the same concrete class
Generated equality compares field values in definition order and generally requires both operands to have the identical class. A dataclass subclass with the same values does not automatically compare equal to its base instance.
Fields can opt out with compare=False. That choice also affects generated ordering methods when order=True. Ordering is tuple-like across the compared fields and similarly requires matching types.
Use ClassVar for annotations that are class configuration rather than instance fields. Use InitVar for constructor inputs passed to __post_init__ but not stored as normal fields.
Hash generation follows mutability assumptions
When eq=True and frozen=False, a dataclass normally sets __hash__ to None, making instances unhashable. A value whose equality-relevant fields can change would be unsafe as a dictionary key.
With eq=True and frozen=True, a field-based hash is normally generated. With eq=False, the superclass hash behaviour is left in place. unsafe_hash=True forces generation, placing responsibility on the author to ensure hash-relevant state does not change while the object is in a set or dictionary.
Individual fields can use hash=False, commonly when a field is expensive to hash but equality should still inspect it. That asymmetry deserves measurement and documentation.
Frozen is recursively immutable
A frozen dataclass installs methods that reject field assignment and deletion after initialisation. It also recursively freezes mutable values stored in its fields, so a list or dictionary inside a frozen instance cannot be changed through another reference.
@dataclass(frozen=True)
class Snapshot:
values: list[int]
Here snapshot.values.append(1) raises FrozenInstanceError, just like assigning a new list to snapshot.values.
Generated __init__ for a frozen class uses low-level assignment so construction can complete. Carefully written __post_init__ code can use object.__setattr__ for derived fields, but doing so later bypasses the intended policy.
Dataclasses are clearest when their flags describe a coherent model: identity-like mutable entities often use eq=False, while immutable values use frozen fields that are themselves immutable and hashable.