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=Truemakes instances immutable (assignment raisesFrozenInstanceError) and hashable, so they can be dict keys or set membersorder=Trueadds<,<=etc., comparing fields in declaration order like a tupleslots=True(3.10+) generates__slots__, saving memory and speeding attribute accesskw_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
@dataclasswrites__init__,__repr__and__eq__from annotated fields- Use
field(default_factory=list)for mutable defaults frozen=Truegives immutable, hashable value objects;replace()makes changed copies
Watch out for
- Assuming type annotations are validated at runtime.