Skip to content
Commitpedia
Log in Join & edit

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?

Edit history of Dataclasses in depth: 1 recorded edit
Edit history · 1 edit
Type
Article
Edits
1
Editors
1
First issued
15 Oct 2026
Last edit
15 Oct 2026
Reading
3 min

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) and astuple(obj) convert recursively, which is convenient before json.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.

  1. Commitpedia +4,141 chars

Report a problem

Something wrong, unsafe or copied? Tell the editors.
What is wrong?