Python

Python Dataclasses👨‍💻

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.

Key Takeaways

  • 1`@dataclass` auto-generates `__init__`, `__repr__`, and `__eq__` from annotated class fields — you write the schema, Python writes the plumbing
  • 2`field(default_factory=list)` is mandatory for mutable defaults — plain `tags: list = []` is a shared-reference bug that the decorator explicitly rejects
  • 3`frozen=True` makes instances immutable by raising `FrozenInstanceError` on attribute assignment — use this for value objects, cache keys, and anything that needs to be hashable
  • 4`slots=True` (Python 3.10+) generates `__slots__` automatically, cutting per-instance memory by ~40% and improving attribute access speed
  • 5`kw_only=True` (Python 3.10+) forces all fields to be keyword-only in the generated `__init__`, preventing positional argument bugs in classes with many fields
  • 6`__post_init__` runs immediately after the generated `__init__`, giving you a clean hook for validation, computed fields, and cross-field consistency checks

Master python dataclasses

Take the Python Fundamentals course with hands-on lessons and challenges.

Examples

API response model with frozen dataclass

python

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.

Configuration object with defaults and __post_init__ validation

python

__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.

Event system with kw_only and slots for performance

python

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.

Database row mapping with field metadata

python

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.

Immutable value object with custom hashing and match_args

python

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.

Comparison: dataclass vs NamedTuple vs Pydantic

python

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).

Common Mistakes

Mistake:

Using a mutable default value directly — `tags: list[str] = []` — which would share the same list across all instances

Fix:

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.

Mistake:

Putting fields with defaults before fields without defaults in a subclass, causing a `TypeError`

Fix:

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.

Mistake:

Using `frozen=True` and then trying to set computed fields in `__post_init__` with normal assignment

Fix:

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.

Mistake:

Expecting `asdict()` to be fast on deeply nested dataclasses — it recursively copies everything

Fix:

`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.

Best Practices

  • Always use `slots=True` on Python 3.10+ unless you need `__dict__` for dynamic attribute assignment or multiple inheritance with non-slotted classes — the memory and speed gains are free
  • Use `frozen=True` for any data object that represents a value (money, coordinates, configuration snapshots) — immutability prevents an entire category of bugs and makes objects hashable
  • Prefer `kw_only=True` on classes with more than 3-4 fields to prevent positional argument mix-ups — `Event(order_id="x", customer_id="y")` is far less error-prone than `Event("x", "y")`
  • Keep `__post_init__` for validation and computed fields only — if you're doing I/O, database calls, or heavy computation in there, the class is doing too much
  • Use `field(metadata={...})` to attach domain-specific information (column names, serialization hints, OpenAPI metadata) instead of inventing parallel mapping dictionaries
  • Prefer dataclasses over Pydantic for internal domain models that never touch external input — save Pydantic for API boundaries where you actually need type coercion and schema generation

Summary

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.

Practice Python with hands-on challenges

Learn python dataclasses hands-on in your IDE

Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.

Related Concepts

Related Cheatsheets

Master Python with Stanza

Interactive lessons and challenges, right in your code editor.

Check the free courses. No credit card.