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