Dataclasses in depth
@dataclass writes __init__, __repr__ and __eq__ for classes that mainly hold data. Learn defaults, default_factory, frozen and ordered classes, __post_init__ validation, slots and kw_only.
Not checked by a trusted editor yet Last edited Outdated?
Many classes exist only to carry a few named values. Writing their __init__, a readable __repr__ and an __eq__ by hand is repetitive and easy to get wrong. The dataclasses module reads the type-annotated fields of a class and generates those methods for you.
from dataclasses import dataclass, field
@dataclass
class Book:
title: str
author: str
pages: int = 0
tags: list[str] = field(default_factory=list)
b = Book("Dune", "Frank Herbert", 412)
print(b) # Book(title='Dune', author='Frank Herbert', pages=412, tags=[])
b == Book("Dune", "Frank Herbert", 412) # True: compares field by field
The annotations are required (they are how fields are detected) but they are not enforced at runtime: Book(title=5, author=None) works. Use a type checker such as mypy or pyright to catch those mistakes, or validate in __post_init__.
Mutable defaults need default_factory
Writing tags: list = [] raises ValueError at class definition time. The dataclass is protecting you: a single list object would be shared by every instance. field(default_factory=list) calls list() fresh for each new object. The same applies to dicts and sets.
Validation and derived fields with __post_init__
@dataclass
class Order:
order_id: str
quantity: int
unit_price: float
total: float = field(init=False)
note: str | None = field(default=None, repr=False)
def __post_init__(self):
if self.quantity < 1:
raise ValueError(f"quantity must be positive, got {self.quantity}")
self.total = self.quantity * self.unit_price
Order("A1", 3, 2.5) # Order(order_id='A1', quantity=3, unit_price=2.5, total=7.5)
init=False keeps a field out of the constructor; repr=False hides it from the printed form, which is useful for secrets or bulky data.
Immutable objects with frozen=True
from dataclasses import replace
@dataclass(frozen=True)
class Point:
x: float
y: float
def moved(self, dx: float, dy: float) -> "Point":
return replace(self, x=self.x + dx, y=self.y + dy)
p = Point(1, 2)
p.x = 5 # raises dataclasses.FrozenInstanceError
p.moved(1, 1) # Point(x=2, y=3), a new object
{p: "label"} # frozen dataclasses are hashable
Frozen objects are safe to share, cache and use as dict keys. dataclasses.replace() builds a modified copy. Note that freezing is shallow: a list stored inside a frozen object can still be changed.
Ordering
@dataclass(order=True)
class Version:
major: int
minor: int
patch: int = 0
sorted([Version(1, 10), Version(1, 2, 5), Version(0, 9)])
# [Version(0, 9, 0), Version(1, 2, 5), Version(1, 10, 0)] (repr shortened)
order=True compares fields as a tuple, in declaration order. Use field(compare=False) to exclude a field from both equality and ordering.
slots and kw_only (Python 3.10+)
@dataclass(slots=True, kw_only=True)
class Config:
host: str
port: int = 5432
debug: bool = False
Config(host="db.local", debug=True) # positional arguments are rejected
slots=True generates __slots__: instances use less memory, attribute access is slightly faster, and typos such as cfg.prot = 1 raise AttributeError instead of silently creating a new attribute. kw_only=True forces keyword arguments, which keeps call sites readable and lets you add fields later without breaking callers.
Helpers
asdict(obj)andastuple(obj)convert recursively, which is convenient beforejson.dumps.fields(obj)lists field metadata for generic code such as CSV writers.field(metadata={...})lets you attach your own information to a field for such tools.
When to use something else
Dataclasses do not convert or validate input types. For data coming from JSON, forms or APIs, libraries such as Pydantic or attrs with validators are a better fit. For a fixed, lightweight record that should behave like a tuple, typing.NamedTuple is enough. For plain internal data with behavior attached, dataclasses are usually the simplest choice.
Did this work for you?
No votes yet. Be the first.
If it did not work, the report form below tells the editors what went wrong.
See also
- 0002 Python cheat sheet: strings, lists, dicts and f-strings The everyday methods for text, lists and dictionaries, plus f-string formatting and comprehensions, each with a one-line example you can paste into… Beginner · Cheat sheet · Any system Article Basics 15 Oct 2026
- 0340 Build and run a small Python web app in a container Write a tiny Flask app, give it a Dockerfile, build the image, run it with a published port, make a change and… Beginner · Guided project · Linux, macOS, Windows Article Basics · Docker basics · Images & registries 15 Oct 2026
History
Every saved edit is kept. The plot on the sleeve draws one line per edit. Open any edit to see exactly what changed.
- Commitpedia +4,141 chars