Dataclasses strip away the boilerplate of writing __init__, __repr__, and __eq__ for classes whose primary job is holding data. You annotate the fields, and the @dataclass decorator generates the rest. They shipped in Python 3.7 and have been steadily gaining features since — slots=True and kw_only=True in 3.10, match_args for structural pattern matching, and tighter integration with typing.
The stdlib dataclasses module deliberately stays minimal. It generates dunder methods and gets out of your way. No schema validation, no serialization magic, no metaclass tricks. That simplicity is the whole point: when you need a structured data container without pulling in a third-party library, dataclasses are the right tool. When you need runtime validation or JSON schema generation, reach for Pydantic instead.
Master python dataclasses
Take the Python Fundamentals course with hands-on lessons and challenges.
Frozen dataclasses are perfect for value objects that should never change after creation. The immutability guarantee makes them safe to use as dictionary keys and in sets, and eliminates a whole class of mutation bugs in concurrent code.
__post_init__ is the right place for cross-field validation. It fires after all fields are set, so you can check relationships between them. Combine this with a @property for computed values that depend on the validated state.
kw_only=True prevents callers from accidentally swapping positional arguments like order_id and customer_id. Combined with slots=True, you get memory-efficient event objects that are safe to instantiate in high-throughput systems like event buses or message queues.
The field() metadata parameter is an underused feature. It attaches arbitrary data to each field that you can introspect at runtime via fields(). This pattern is how lightweight ORM mappers and serialization libraries work under the hood.
Frozen dataclasses are natural value objects. Because frozen=True generates __hash__ automatically, Money instances work as dict keys and set members. The match_args parameter (True by default) enables clean structural pattern matching without extra boilerplate.
Use NamedTuple for simple immutable records where tuple behavior (indexing, unpacking) is useful. Use dataclasses for general-purpose structured data in application code. Use Pydantic when you need runtime type coercion, JSON serialization, or OpenAPI schema generation (APIs, config files, external data).
Using a mutable default value directly — `tags: list[str] = []` — which would share the same list across all instances
Use `field(default_factory=list)` for any mutable default. The dataclass decorator actually raises a `ValueError` if you try `= []`, but beginners often work around it with `= None` and then mutate in `__post_init__`, which is equally broken. Let `default_factory` handle it.
Putting fields with defaults before fields without defaults in a subclass, causing a `TypeError`
Dataclass inheritance concatenates parent + child fields. If the parent has `name: str = ""` and the child adds `id: int` (no default), the generated `__init__` has a non-default after a default. Fix by making the child field keyword-only: `id: int = field(kw_only=True)` or restructure the hierarchy so defaults come last.
Using `frozen=True` and then trying to set computed fields in `__post_init__` with normal assignment
In a frozen dataclass, use `object.__setattr__(self, 'field_name', value)` inside `__post_init__` to bypass the freeze. This is the official escape hatch documented in the stdlib. It only works during initialization.
Expecting `asdict()` to be fast on deeply nested dataclasses — it recursively copies everything
`asdict()` creates a full deep copy by calling itself on nested dataclasses, lists, tuples, and dicts. For hot paths, write a custom `to_dict()` method or use `{f.name: getattr(self, f.name) for f in fields(self)}` for a shallow one-level conversion.
Dataclasses generate `__init__`, `__repr__`, `__eq__`, and optionally `__hash__` and ordering methods from type-annotated fields. Use `field()` for mutable defaults and metadata, `frozen=True` for immutable value objects, `slots=True` for memory efficiency, and `kw_only=True` to prevent positional argument mistakes. `__post_init__` handles validation and computed fields. For pure internal data containers, dataclasses beat both NamedTuple (more features, mutable option) and Pydantic (no runtime overhead, no dependency). Reach for Pydantic when you need validation of external data, and NamedTuple when you want an immutable, iterable record with zero overhead.
Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.
Interactive lessons and challenges, right in your code editor.
Check the free courses. No credit card.