Filter concepts by levelShowing all levels.

Python · Dataclasses and Data Modeling

Dataclasses and Data Modeling

Generating __init__/__repr__/__eq__ from a field list, and when a dataclass is the right tool at all

  • @dataclassgenerates __init__, __repr__, __eq__ from annotated fields
  • frozen=Trueraises FrozenInstanceError on any assignment after construction
  • default_factoryrequired for a mutable default — a plain [] raises ValueError
  • Pydantic validates@dataclass only generates methods, it never checks types at runtime

1.@dataclass

@dataclass reads a class's type-annotated attributes and generates __init__, __repr__, and __eq__ for you. You still write the attribute list; Python writes the boilerplate methods that normally go with it.

Key features

  • @dataclass sits above a class whose body is just NAME: TYPE annotations
  • Generates __init__(self, field1, field2, ...) from the annotated fields, in order
  • Generates __repr__ as ClassName(field1=value1, field2=value2, ...)
  • Generates __eq__ that compares two instances field by field, not by identity
  • Annotations are never enforced at runtime — @dataclass reads them, does not validate types
What @dataclass generates from three annotated fields

Annotated fields

order_id: str, total_cents: int, is_paid: bool = False

@dataclass reads them

at class-creation time, once

__init__, __repr__, __eq__

generated and attached to the class

  1. Annotated fields — order_id: str, total_cents: int, is_paid: bool = False
  2. @dataclass reads them — at class-creation time, once
  3. __init__, __repr__, __eq__ — generated and attached to the class

Command

python
from dataclasses import dataclass

@dataclass
class Order:
    order_id: str
    total_cents: int
    is_paid: bool = False   # a default value, same rules as a function default

@dataclass generates __init__, __repr__, and __eq__ from type-annotated fields — it never validates or enforces those types at runtime.

2.Frozen dataclasses

@dataclass(frozen=True) blocks assigning to any field after construction. Attempting obj.field = value raises dataclasses.FrozenInstanceError instead of silently changing the object.

Key features

  • @dataclass(frozen=True) — one keyword argument added to the decorator
  • obj.field = value raises dataclasses.FrozenInstanceError, a subclass of AttributeError
  • Fields are still readable normally — only assignment after __init__ is blocked
  • __init__ itself is allowed to set fields once, during construction
  • Frozen instances are hashable by default if eq is also on, unlike regular dataclasses

Command

python
from dataclasses import dataclass

@dataclass(frozen=True)
class Coordinates:
    lat: float
    lon: float

frozen=True raises FrozenInstanceError (an AttributeError subclass) on any assignment after construction — use object.__setattr__ inside __post_init__.

3.Default values, default_factory, and field()

field: type = value gives a field a plain default. A mutable default like [] is rejected at class-creation time — field(default_factory=list) calls a zero-argument function per instance instead, avoiding a shared, silently-mutated default.

Key features

  • field: type = value — a plain default, safe only for immutable values (int, str, bool)
  • field: list = [] at class level raises ValueError at class-creation time — not allowed
  • field(default_factory=list) calls list() fresh for every new instance instead
  • field() also accepts init, repr, compare, and metadata — see the table below
  • A field with no default must come before every field that has one, left to right

field() keyword arguments

field() keyword arguments
ArgumentDefaultEffect
defaultMISSINGA plain default value — mutually exclusive with default_factory
default_factoryMISSINGA zero-argument callable run once per instance to produce the default
initTrueFalse excludes the field from the generated __init__ parameter list
reprTrueFalse excludes the field from the generated __repr__ output
compareTrueFalse excludes the field from the generated __eq__/ordering comparisons
metadata{}A read-only mapping for third-party tools — @dataclass itself ignores it

Command

python
from dataclasses import dataclass, field

@dataclass
class Cart:
    items: list = field(default_factory=list)   # NOT items: list = []

A plain default is fine for immutable values; a mutable one raises ValueError at class-creation time — use field(default_factory=list) instead.

4.Post-init processing

If a dataclass defines __post_init__(self), the generated __init__ calls it automatically as its last step, after every field is already assigned. It is where validation or a computed field belongs.

Key features

  • def __post_init__(self): runs automatically, as the last step of the generated __init__
  • Every field is already assigned to self by the time __post_init__ runs
  • Common uses: validating a value, or computing one field from the others
  • field(init=False) marks a field the caller cannot pass — __post_init__ sets it instead
  • A frozen dataclass still runs __post_init__ — use object.__setattr__ inside it to assign

Command

python
from dataclasses import dataclass, field

@dataclass
class Rectangle:
    width: float
    height: float
    area: float = field(init=False)   # not passed in — computed instead

    def __post_init__(self):
        self.area = self.width * self.height

__post_init__(self) runs as the last step of the generated __init__, once every field is assigned — use it for validation or a field(init=False) value.

5.Ordering

@dataclass(order=True) generates __lt__, __le__, __gt__, and __ge__, comparing instances field by field in declaration order — like comparing the tuples of their values. Without it, < and > raise TypeError.

Key features

  • @dataclass(order=True) generates __lt__, __le__, __gt__, __ge__ — not just one of them
  • Comparison works field by field, in the order fields are declared — like tuple comparison
  • Without order=True, obj1 < obj2 raises TypeError: not supported between instances
  • sorted() and min()/max() work on a list of instances once order=True is set
  • eq=True (the default) is required alongside order=True — dataclass raises otherwise

Command

python
from dataclasses import dataclass

@dataclass(order=True)
class Version:
    major: int
    minor: int
    patch: int

@dataclass(order=True) compares instances field by field, in declaration order, like tuples — without it, < and > raise TypeError.

6.Dataclass inheritance

A dataclass subclassing another dataclass inherits its fields, in order, before its own. The combined field list still needs every no-default field before every field with a default, across both classes.

Key features

  • A dataclass subclass of a dataclass gets one combined __init__, parent fields first
  • fields(SubClass) shows parent fields before the subclass's own, in that order
  • A parent field with a default, followed by a subclass field without one, raises TypeError
  • A subclass needs its own @dataclass decorator too, or it generates no methods of its own
  • Overriding a parent field's default in a subclass keeps its original position in the list

Command

python
from dataclasses import dataclass

@dataclass
class Animal:
    name: str
    sound: str = "..."

@dataclass
class Dog(Animal):
    breed: str = "mixed"   # must also have a default -- sound already does

A dataclass subclass gets one combined __init__, parent fields first — the no-default-before-default rule spans the whole combined list, not one class.

7.slots=True

@dataclass(slots=True), added in Python 3.10, generates __slots__ from the field list automatically — no __dict__, lower memory, and AttributeError on any attribute not declared as a field.

Key features

  • slots=True (Python 3.10+) generates __slots__ from the dataclass's own field names
  • Requires no manual __slots__ = (...) — the decorator derives it from the fields
  • Instances have no __dict__ — assigning an undeclared attribute raises AttributeError
  • slots=True rebuilds the class object, since __slots__ must exist at class-creation time
  • Combine with frozen=True for a memory-efficient, immutable value object

Command

python
from dataclasses import dataclass

@dataclass(slots=True)
class Point3D:
    x: float
    y: float
    z: float

slots=True (3.10+) generates __slots__ from the dataclass's own fields — same no-__dict__ guarantee as hand-written __slots__, without a second list to sync.

8.Dataclasses vs regular classes vs Pydantic models, and value objects

A dataclass trades a small amount of control for generated __init__/__repr__/__eq__ — reach for a regular class when you need more control over construction, and Pydantic when you need runtime validation, not just type hints.

Key features

  • A dataclass is a regular class plus generated methods — instances behave identically
  • Pydantic (BaseModel) validates and coerces values at construction; @dataclass does not
  • Pydantic raises ValidationError listing every invalid field at once, not just the first
  • A value object is any immutable type compared by VALUE — usually a frozen dataclass
  • Choose a regular class when construction needs logic a generated __init__ cannot express

Regular class vs @dataclass vs Pydantic BaseModel

Regular class vs @dataclass vs Pydantic BaseModel
PropertyRegular class@dataclassPydantic BaseModel
__init__/__repr__/__eq__written by handgeneratedgenerated
Runtime type validationnone, unless you add itnone — hints are uncheckedyes — raises ValidationError
Coerces input ("3" -> 3)nonoyes, by default
Extra dependencynono (stdlib)yes (pydantic)
Typical usecustom construction logicinternal data holdersdata crossing a trust boundary (API input, config, files)

Command

python
from dataclasses import dataclass

@dataclass(frozen=True, slots=True)   # a value object: immutable, compared by value
class Money:
    amount_cents: int
    currency: str

A dataclass generates boilerplate but never validates; Pydantic validates and coerces at construction. A value object is compared by value, not identity.

Remember

  • @dataclass generates __init__/__repr__/__eq__ from type-annotated fields — it never validates those types at runtime.
  • A mutable default (list, dict, set) raises ValueError at class-creation time — use field(default_factory=list) instead.
  • __post_init__ runs automatically as the last step of the generated __init__, once every field is already assigned.
  • frozen=True raises FrozenInstanceError (an AttributeError subclass) on any assignment after construction.

Pro tip

  • order=True compares instances field by field, like tuples — eq must stay True alongside it, or class creation raises.
  • slots=True (3.10+) generates __slots__ from the field list — same no-__dict__ guarantee as writing it by hand.
  • A dataclass subclass gets ONE combined __init__ — parent fields first — so the no-default-after-default rule spans both classes.
  • Reach for Pydantic, not a dataclass, for anything crossing a trust boundary — an API body, a config file, a CSV row.

Next up

  • Standard Library

Sources

  • dataclasses — docs.python.org/3/library/dataclasses.html
  • Pydantic models — docs.pydantic.dev/latest/concepts/models/
  • Every dataclass example executed on Python 3.14.3 · checked 2026-08-21

Spotted a mistake on this page? Tell us.