Cheatsheet for value classes.
stdlib @dataclass
from dataclasses import dataclass, field
@dataclass
class User:
id: int
name: str
tags: list[str] = field(default_factory=list)
created_at: datetime = field(default_factory=datetime.utcnow)
field(default_factory=...) for mutable defaults.
Frozen + slots
@dataclass(frozen=True, slots=True)
class Point:
x: float
y: float
frozen: immutable (raises FrozenInstanceError on set).
slots: no __dict__; less memory; faster attribute access.
post_init
@dataclass
class User:
first: str
last: str
full_name: str = ""
def __post_init__(self):
self.full_name = f"{self.first} {self.last}"
Runs after __init__.
init=False / repr=False
@dataclass
class User:
id: int
name: str
_internal: dict = field(init=False, repr=False, default_factory=dict)
_internal not in __init__ or __repr__.
kw_only
@dataclass(kw_only=True)
class User:
id: int
name: str
User(id=1, name="x") # works
User(1, "x") # error
Forces keyword args.
eq / order
@dataclass(order=True)
class Item:
priority: int
name: str = field(compare=False)
Sortable by priority; name not used in comparison.
attrs (third-party)
from attrs import define, field
@define
class User:
id: int
name: str
tags: list[str] = field(factory=list)
@define is the modern attrs decorator.
attrs validators
from attrs import define, field, validators
@define
class User:
age: int = field(validator=validators.ge(0))
email: str = field(validator=validators.matches_re(r"^[^@]+@[^@]+$"))
attrs converters
@define
class User:
name: str = field(converter=str.strip)
age: int = field(converter=int)
User(name=" alice ", age="25") → User(name="alice", age=25).
attrs vs dataclass
| dataclass | attrs | |
|---|---|---|
| Built-in | yes | install |
| Validators / converters | manual | built-in |
| frozen | yes | yes |
| slots | yes | yes |
| Speed | similar | slightly faster |
| API | familiar | richer |
For most: dataclass is enough. Reach for attrs when validators / converters help.
Pydantic vs dataclass
Pydantic: validation at boundary (input). Dataclass: internal value types, no runtime validation.
For API input: Pydantic. For internal value types: dataclass.
Copy / replace
from dataclasses import replace
new_user = replace(user, name="X")
attrs:
from attrs import evolve
new_user = evolve(user, name="X")
Immutable-style update.
asdict / astuple
from dataclasses import asdict, astuple
asdict(user) # {"id": 1, "name": "..."}
astuple(user) # (1, "...")
attrs:
from attrs import asdict, astuple
Inheritance
@dataclass
class Base:
id: int
@dataclass
class User(Base):
name: str
User has both id and name.
For frozen inheritance: both must be frozen.
Performance
slots=True is the biggest perf win:
- Faster attribute access.
- Lower memory.
- No
__dict__.
Use it on hot value types.
Comparison
@dataclass
class Point:
x: float
y: float
Point(1, 2) == Point(1, 2) # True
Auto-generated __eq__ via field values.
When NOT to use dataclass
- Real domain objects with rich behavior — use regular classes.
- Pydantic-like validation needs — use Pydantic.
- Need lazy / computed attributes — use properties.
Common mistakes
- Mutable default without
default_factory— shared across instances. slots=True+ multiple inheritance — quirky.- Comparing two dataclasses of different types — equality is per-class.
- Hashing — frozen dataclasses are hashable; mutable are not (by default).
Read this next
If you want my dataclass/attrs decision guide + benchmarks, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .