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

dataclassattrs
Built-inyesinstall
Validators / convertersmanualbuilt-in
frozenyesyes
slotsyesyes
Speedsimilarslightly faster
APIfamiliarricher

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 .