backend / python core / stdlib / 05_dataclasses.md

dataclasses — boilerplate-free data classes

3 min read source

dataclasses — boilerplate-free data classes

@dataclass (3.7+) generates __init__, __repr__, __eq__, and optionally other dunder methods, based on class-level type-annotated fields.

Basics

from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int
    label: str = "origin"   # default value

p = Point(1, 2)
print(p)               # Point(x=1, y=2, label='origin')
p == Point(1, 2)       # True — auto __eq__ on field tuple

Equivalent without @dataclass is ~15 lines. Decorator does:

  • __init__(self, x, y, label="origin")
  • __repr__ showing all fields
  • __eq__ comparing field tuples

Decorator parameters

@dataclass(
    init=True,         # generate __init__ (default True)
    repr=True,         # generate __repr__
    eq=True,           # generate __eq__
    order=False,       # generate __lt__, __le__, __gt__, __ge__ (compares as tuple)
    frozen=False,      # make instances immutable (no attr setting)
    unsafe_hash=False, # force generate __hash__
    slots=False,       # 3.10+: generate __slots__
    kw_only=False,     # 3.10+: all fields keyword-only
)
class C: ...

field() — per-field configuration

from dataclasses import dataclass, field

@dataclass
class Article:
    title: str
    tags: list[str] = field(default_factory=list)        # mutable default fix
    metadata: dict = field(default_factory=dict)

    _cache: dict = field(default_factory=dict, repr=False, compare=False)
    # excluded from __repr__ and __eq__

    id: int = field(init=False, default=0)
    # not in __init__; set in __post_init__ or elsewhere

Why default_factory? Because tags: list = [] would share the same list across all instances — see tricky_questions/18_dataclass_mutable_default.md. Dataclass actually rejects this with a clear error.

__post_init__ — extra setup

Runs at the end of __init__:

@dataclass
class Article:
    title: str
    body: str
    word_count: int = field(init=False)

    def __post_init__(self):
        self.word_count = len(self.body.split())

a = Article("Hello", "world foo bar")
print(a.word_count)   # 3

Use for: derived fields, validation, normalizing strings.

frozen=True — immutability

@dataclass(frozen=True)
class Point:
    x: int
    y: int

p = Point(1, 2)
p.x = 5    # FrozenInstanceError

Frozen dataclasses are hashable by default (__hash__ generated based on field tuple). Useful as dict keys / set members.

slots=True (3.10+)

@dataclass(slots=True)
class Point:
    x: int
    y: int

Equivalent to writing __slots__ = ("x", "y"). Saves memory for many instances. See 28_slots.md.

kw_only=True (3.10+)

@dataclass(kw_only=True)
class Config:
    host: str
    port: int = 8080

Config(host="localhost", port=80)   #
Config("localhost", 80)             # TypeError — must be keyword

Or per-field:

@dataclass
class Config:
    name: str
    port: int = field(kw_only=True)   # only `port` is kw-only

This solves the “default-before-non-default” inheritance problem:

@dataclass
class Base:
    a: int = 0

@dataclass
class Child(Base):
    b: int   # error: non-default after default

# Fix:
@dataclass
class Child(Base):
    b: int = field(kw_only=True)

asdict, astuple, replace, fields

from dataclasses import asdict, astuple, replace, fields

p = Point(1, 2)
asdict(p)      # {'x': 1, 'y': 2}
astuple(p)     # (1, 2)
replace(p, x=10)   # Point(x=10, y=2) — new instance, p unchanged

for f in fields(p):
    print(f.name, f.type, f.default)

asdict recurses into nested dataclasses, lists, and dicts.

dataclass vs pydantic.BaseModel vs attrs

@dataclass pydantic.BaseModel @attrs.define
Stdlib yes no no
Validation no runtime optional
Coercion no yes optional
Serialization (JSON) manual built-in optional
Speed fastest very fast (Rust core in v2) fast
Use case internal data classes API models, config richer than stdlib, no runtime cost by default

For internal value objects, @dataclass is the right default. For HTTP request/response or config validation, use Pydantic.

Interview angle

“What does @dataclass generate?” (__init__, __repr__, __eq__.) “Why use field(default_factory=list) instead of = []?” (Mutable default sharing.) “When use a frozen dataclass?” (When you need it as a dict key or want immutability for safety.)