Complete reference for dataclasses (Python 3.9 baseline, newer features tagged). Examples use AI-engineering objects but every rule is general.
Contents
- What the decorator does
- Decorator options
- field() parameters
- Default values and default_factory
- post_init and InitVar
- Keyword-only fields and KW_ONLY
- Inheritance and field ordering
- Equality, ordering and hashing
- frozen=True
- slots=True and weakref_slot
- Module functions
- ClassVar, descriptors and properties
- Pattern matching
- Generics
- Performance and memory
- Error catalogue
What the decorator does
@dataclass inspects the class __annotations__ at class-creation time. Every annotated name that is not a ClassVar (and not an InitVar, which is a pseudo-field) becomes a field, in definition order. It then adds methods to the class: __init__, __repr__, __eq__, and optionally ordering methods, __hash__, __setattr__/__delattr__ (frozen), __match_args__, __slots__. It does not add runtime type checking: annotations are documentation for type checkers.
from dataclasses import dataclass
@dataclass
class TokenUsage:
prompt_tokens: int
completion_tokens: int = 0
u = TokenUsage(12, 30)
print(u) # TokenUsage(prompt_tokens=12, completion_tokens=30)
print(u == TokenUsage(12, 30)) # True
TokenUsage("12") # accepted at runtime: no validationRules for whether a generated method replaces one you wrote:
| Method | If you define it in the class body |
|---|---|
__init__, __repr__, __eq__ | yours is kept (decorator skips it) |
__lt__/__le__/__gt__/__ge__ with order=True | TypeError |
__setattr__/__delattr__ with frozen=True | TypeError |
__hash__ | see hash table |
Introspection attributes: cls.__dataclass_fields__ (dict name -> Field), cls.__dataclass_params__, cls.__match_args__ (3.10+).
Decorator options
@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False,
match_args=True, kw_only=False, slots=False, weakref_slot=False)| Option | Default | Generates / changes | Since |
|---|---|---|---|
init | True | __init__(self, <init fields>); calls __post_init__ if defined | 3.7 |
repr | True | __repr__ as Cls(a=1, b='x') over repr=True fields; recursion-safe | 3.7 |
eq | True | __eq__ comparing compare-fields in order; NotImplemented for a different class | 3.7 |
order | False | __lt__, __le__, __gt__, __ge__ comparing field tuples; requires eq=True | 3.7 |
unsafe_hash | False | force a field-based __hash__ even when mutable | 3.7 |
frozen | False | assignment/deletion raise FrozenInstanceError | 3.7 |
match_args | True | __match_args__ = names of positional init fields | 3.10 |
kw_only | False | every field keyword-only in __init__ | 3.10 |
slots | False | returns a new class with __slots__ | 3.10 |
weakref_slot | False | add __weakref__ slot; requires slots=True | 3.11 |
The decorator can be used bare (@dataclass) or called (@dataclass(frozen=True)).
Choosing options quickly
| Need | Options |
|---|---|
| Plain mutable record | @dataclass |
| Config / cache key / dict key | @dataclass(frozen=True) |
| Many instances (chunks, tokens, events) | @dataclass(slots=True) 3.10+ |
| Immutable and compact | @dataclass(frozen=True, slots=True) |
| Sortable results | @dataclass(order=True) with sort key first, or field(compare=False) on the rest |
| Identity semantics (objects with resources, graph nodes) | @dataclass(eq=False) |
| Long constructor, avoid positional mistakes | @dataclass(kw_only=True) 3.10+ |
field() parameters
dataclasses.field(*, default=MISSING, default_factory=MISSING, init=True, repr=True,
hash=None, compare=True, metadata=None, kw_only=MISSING, doc=None)| Parameter | Meaning | Typical use |
|---|---|---|
default | default value (immutable!) | field(default=0, repr=False) when combining with other options |
default_factory | zero-arg callable invoked per instance | list, dict, lambda: ["\n"], uuid.uuid4, time.time |
init | include in __init__ | False for derived/internal state |
repr | include in __repr__ | False for secrets, embeddings, big blobs |
hash | include in __hash__; None = follow compare | False for a field compared but not hashed |
compare | include in __eq__/ordering | False for arrays, timestamps, caches |
metadata | read-only mapping, ignored by dataclasses | JSON aliases, units, CLI help |
kw_only | this field keyword-only | 3.10+ |
doc | docstring for the field (shown by help()) | 3.14+ |
default and default_factory are mutually exclusive (ValueError). field(init=False) without a default leaves the attribute unset until __post_init__ assigns it; repr() before that raises AttributeError.
import time
import uuid
from dataclasses import dataclass, field, fields
@dataclass
class TraceSpan:
name: str
span_id: str = field(default_factory=lambda: uuid.uuid4().hex)
started_at: float = field(default_factory=time.time, compare=False)
attributes: dict = field(default_factory=dict)
cost_usd: float = field(default=0.0, metadata={"unit": "USD"})
print([(f.name, f.metadata.get("unit")) for f in fields(TraceSpan)])Default values and default_factory
Defaults are evaluated once at class creation and stored as class attributes. That is why mutable defaults are rejected:
from dataclasses import dataclass, field
@dataclass
class Bad:
tags: list = [] # ValueError: mutable default <class 'list'> for field tags is not allowed: use default_factory
@dataclass
class Good:
tags: list = field(default_factory=list)- Before 3.11 the check rejected only
list,dict,set. Since 3.11 it rejects any default whose class is unhashable (__hash__ is None), e.g. an instance of a non-frozen dataclass witheq=True. - Hashable but mutable objects (a
Tokenizer()instance, arequests.Session()) pass the check and are shared by all instances. Usedefault_factory=Tokenizer. - A factory with arguments:
field(default_factory=lambda: defaultdict(list))orfunctools.partial(...). default_factoryis not called if a value is passed to__init__.- Fields with
init=Falseand adefault_factorystill get the factory value in__init__.
Immutable alternatives that work as plain defaults: (), frozenset(), "", 0, None, enum members, instances of frozen dataclasses. A read-only mapping (MappingProxyType) is unhashable, so give it a factory.
post_init and InitVar
The generated __init__ assigns fields and then calls self.__post_init__(*initvars) if the method exists.
from dataclasses import InitVar, dataclass, field
from typing import Optional
@dataclass
class Chunk:
text: str
doc_id: str
tokenizer: InitVar[Optional[object]] = None
n_tokens: int = field(init=False)
normalized: str = field(init=False, repr=False)
def __post_init__(self, tokenizer: Optional[object]) -> None:
if not self.text:
raise ValueError("empty chunk")
self.normalized = " ".join(self.text.split())
self.n_tokens = len(tokenizer.encode(self.text)) if tokenizer else len(self.text) // 4Rules:
InitVar[T]fields appear in__init__(respecting default rules) and are passed positionally to__post_init__in declaration order. They are not stored, not infields(),asdict(),repr,eq.- If a class-level default is given to an
InitVar, that value is also set as a class attribute (a quirk:Chunk.tokenizerisNone). replace()requires everyInitVarwithout a default to be passed explicitly (ValueErroron older versions,TypeErroron recent ones).__post_init__is not called wheninit=Falseis passed to the decorator.- Inheritance: the generated
__init__callsself.__post_init__, which resolves to the most-derived one. Callsuper().__post_init__(...)explicitly when a base also has one. - Do expensive work lazily (a property or
functools.cached_property, not available withslots=True) rather than in__post_init__when objects are created in bulk.
@dataclass
class BaseRecord:
id: str
def __post_init__(self) -> None:
self.id = self.id.strip()
@dataclass
class EvalRecord(BaseRecord):
score: float = 0.0
def __post_init__(self) -> None:
super().__post_init__()
if not 0.0 <= self.score <= 1.0:
raise ValueError("score must be in [0, 1]")Keyword-only fields and KW_ONLY
Three ways (all 3.10+):
from dataclasses import KW_ONLY, dataclass, field
@dataclass(kw_only=True)
class A:
model: str
temperature: float = 0.7
@dataclass
class B:
model: str
_: KW_ONLY # everything after the sentinel is keyword-only
temperature: float = 0.7
seed: int # no default after a default: allowed because keyword-only
@dataclass
class C:
model: str
seed: int = field(kw_only=True, default=0)Keyword-only fields are moved after all positional fields in the generated __init__, regardless of declaration order, and are excluded from __match_args__. On 3.9 there is no keyword-only support: order fields so defaults come last, or write __init__ by hand.
Inheritance and field ordering
Fields are gathered in MRO order (base first). A field redefined in a subclass keeps its original position but takes the new type/default.
from dataclasses import dataclass, field
@dataclass
class BaseRequest:
model: str
temperature: float = 0.7
@dataclass
class ChatRequest(BaseRequest):
messages: list = field(default_factory=list) # has a default: OK
@dataclass
class Broken(BaseRequest):
messages: list # TypeError: non-default argument 'messages' follows default argumentFixes for Broken: give messages a default; make the subclass kw_only=True (3.10+); use field(kw_only=True); or prefer composition (request: BaseRequest).
Other inheritance rules:
| Situation | Result |
|---|---|
| frozen subclass of non-frozen dataclass (or vice versa) | TypeError: cannot inherit frozen dataclass from a non-frozen one |
dataclass subclass of a plain class with its own __init__ | base __init__ is not called by the generated one; call it from __post_init__ or convert the base |
| plain subclass of a dataclass | inherits fields and methods; annotations in the subclass are not fields unless decorated |
slots=True base and subclass | each must be decorated with slots=True for full slot coverage |
| multiple inheritance | fields merged in reverse MRO; keep it shallow |
Equality, ordering and hashing
__eq__ returns NotImplemented unless other.__class__ is self.__class__, then compares compare-fields in order as if they were tuples. Subclass instances are therefore never equal to base instances.
order=True compares (f1, f2, ...) tuples of compare-fields: put the sort key first or use sorted(key=...) instead.
from dataclasses import dataclass, field
@dataclass(order=True)
class RankedHit:
neg_score: float # sort key first: best score first when ascending
chunk_id: str = field(compare=True)
text: str = field(compare=False)
hits = sorted([RankedHit(-0.8, "b", "..."), RankedHit(-0.9, "a", "...")])Hash generation table
__hash__ is decided from unsafe_hash, eq, frozen and whether the class body defines __hash__ explicitly:
| unsafe_hash | eq | frozen | explicit __hash__ in class | Result |
|---|---|---|---|---|
| False | True | False | no | __hash__ = None (unhashable) |
| False | True | True | no | generated from hash-fields |
| False | False | any | no | inherited (object.__hash__, identity) |
| False | any | any | yes | yours is kept |
| True | any | any | no | generated from hash-fields |
| True | any | any | yes | TypeError |
Hash-fields are fields with hash=True, or hash=None and compare=True. Consequences:
- Converting a plain class to
@dataclassmakes instances unhashable:set()of them,lru_cachearguments and dict keys break. Usefrozen=True, oreq=Falseto keep identity. unsafe_hash=Trueon a mutable object: mutating a hashed field while the object sits in asetcorrupts the set.- A frozen dataclass containing a
listornp.ndarrayfield raisesTypeError: unhashable typeonly whenhash()is called: use tuples orhash=False.
frozen=True
from dataclasses import FrozenInstanceError, dataclass, replace
@dataclass(frozen=True)
class SamplingParams:
temperature: float = 0.7
top_p: float = 1.0
p = SamplingParams()
try:
p.temperature = 0.0
except FrozenInstanceError as e: # subclass of AttributeError
print(e) # cannot assign to field 'temperature'
greedy = replace(p, temperature=0.0)- In
__post_init__(or any method) useobject.__setattr__(self, "name", value). - Frozen is shallow:
frozen.items.append(x)still works ifitemsis a list. - Cost: frozen
__init__usesobject.__setattr__for every field, measurably slower for millions of objects. copy.replace(obj, **changes)works on dataclasses from 3.13 (__replace__).
slots=True and weakref_slot
slots=True (3.10+) makes the decorator build a new class with __slots__ derived from the fields.
Benefits: lower memory per instance, faster attribute access, typos like obj.scroe = 1 raise AttributeError.
Caveats:
| Caveat | Detail |
|---|---|
| New class object | decorators applied before @dataclass see the old class; references captured at class creation point to the old one |
Zero-arg super() | broken in methods of slots=True dataclasses on 3.10-3.13 (TypeError), fixed in 3.14; use super(Cls, self) when supporting older versions |
functools.cached_property | needs __dict__; fails with slots=True |
__weakref__ | not available unless weakref_slot=True (3.11+) - needed for weakref.ref, WeakValueDictionary caches |
Manual __slots__ | do not combine with slots=True; a manual __slots__ plus field defaults raises ValueError: 'x' in __slots__ conflicts with class variable |
| Pickling | supported; frozen + slots classes get generated __getstate__/__setstate__ |
| Adding attributes dynamically | impossible (no __dict__) |
On 3.9, emulate with a hand-written __slots__ only when fields have no defaults, or upgrade.
Module functions
| Function | Purpose | Notes |
|---|---|---|
fields(obj_or_cls) | tuple of Field | excludes ClassVar and InitVar |
asdict(obj, *, dict_factory=dict) | recursive dict | deep-copies non-dataclass values |
astuple(obj, *, tuple_factory=tuple) | recursive tuple | same deep-copy behaviour |
replace(obj, /, **changes) | new instance via __init__ | re-runs __post_init__; init=False fields cannot be passed |
is_dataclass(obj) | True for dataclass classes and instances | isinstance(obj, type) to tell them apart |
make_dataclass(name, fields, ...) | build a class dynamically | fields as name, (name, type) or (name, type, field(...)) |
MISSING | sentinel for "no default" | compare with is |
FrozenInstanceError | raised by frozen assignment | subclass of AttributeError |
KW_ONLY | sentinel type (3.10) | _: KW_ONLY |
InitVar | init-only pseudo-field | InitVar[int] |
asdict deep-copy pitfalls
import threading
from dataclasses import asdict, dataclass, field, fields
@dataclass
class Retriever:
index_name: str
_lock: threading.Lock = field(default_factory=threading.Lock, repr=False, compare=False)
r = Retriever("docs")
try:
asdict(r)
except TypeError as e:
print(e) # cannot pickle '_thread.lock' object
def shallow_dict(obj) -> dict:
"""Top-level fields only, no copying, skips private fields."""
return {f.name: getattr(obj, f.name) for f in fields(obj) if not f.name.startswith("_")}asdicthandlesnamedtuplespecially (rebuilds it), lists/tuples/dicts recursively, and callscopy.deepcopyon everything else.- A 1536-dim
float32embedding per chunk times 10 000 chunks gets copied on everyasdictcall. dict_factoryreceives a list of(key, value)pairs, useful to dropNones:
from dataclasses import asdict
def drop_none(pairs):
return {k: v for k, v in pairs if v is not None}
payload = asdict(request, dict_factory=drop_none) # applied at every nesting levelreplace() semantics
from dataclasses import dataclass, field, replace
@dataclass(frozen=True)
class Prompt:
template: str
n_chars: int = field(init=False)
def __post_init__(self) -> None:
object.__setattr__(self, "n_chars", len(self.template))
p2 = replace(Prompt("Hi {name}"), template="Hello {name}")
print(p2.n_chars) # 12: recomputed by __post_init__
# replace(p2, n_chars=3) -> error: field n_chars is declared with init=False, it cannot be specified with replace()make_dataclass
from dataclasses import field, make_dataclass
Row = make_dataclass(
"EvalRow",
[("example_id", str), ("score", float), ("tags", list, field(default_factory=list))],
frozen=False,
)
print(Row("ex1", 0.9))Useful when the schema comes from a config (column list of an eval CSV). Prefer static classes when the shape is known, so type checkers see it.
ClassVar, descriptors and properties
from dataclasses import dataclass
from typing import ClassVar
@dataclass
class EmbeddingModel:
name: str
dim: int
registry: ClassVar[dict] = {} # class-level, not a field, mutable allowed
DEFAULT_BATCH: ClassVar[int] = 64
@property
def is_large(self) -> bool:
return self.dim >= 1024- Unannotated class attributes are not fields either (
MAX = 3). - Properties with the same name as a field interfere with defaults; use a private field plus a property with a different name.
- Descriptor-typed fields: the descriptor's
__get__(None, cls)result is used as the default;__set__is called by__init__.
Pattern matching
__match_args__ (3.10+) enables positional class patterns:
from dataclasses import dataclass
@dataclass
class TextDelta:
text: str
@dataclass
class ToolUseDelta:
tool_name: str
partial_json: str
def render(event) -> str:
match event:
case TextDelta(text):
return text
case ToolUseDelta(name, _):
return f"[calling {name}]"
case _:
return ""Keyword-only fields are not in __match_args__; match them with keyword patterns: case Cfg(model="gpt-4o").
Generics
from dataclasses import dataclass, field
from typing import Generic, TypeVar
T = TypeVar("T")
@dataclass
class Page(Generic[T]):
items: list = field(default_factory=list)
next_cursor: str = ""
pages: "Page[str]" = Page(items=["a", "b"])3.12+ syntax: class Page[T]: with items: list[T] (PEP 695). Subscripting a dataclass generic does not validate anything at runtime.
Performance and memory
Order-of-magnitude guidance (CPython 3.12, measure your own workload):
| Container | Create 1M small objects | Memory per instance | Attribute access |
|---|---|---|---|
plain class / @dataclass | baseline | __dict__ (~100+ bytes) | baseline |
@dataclass(slots=True) | similar or faster | roughly half or less | slightly faster |
@dataclass(frozen=True) | slower (object.__setattr__ per field) | as above | same |
NamedTuple | fast | small (tuple) | index-based, fast |
dict | fast | larger than slots | key lookup |
pydantic v2 BaseModel | several times slower (validation) | larger | same-ish |
msgspec.Struct | fastest | smallest | fast |
Tips:
- Class creation runs
execonce per decorated class: negligible at import, noticeable only formake_dataclassin loops. - For millions of records, columnar storage (numpy arrays, Arrow, DataFrame) beats any per-object container.
slots=Trueis the cheapest memory win for chunks/tokens/events created in bulk.- Avoid
asdict()in hot loops; write a field-by-fieldto_dict().
Error catalogue
| Error | Cause | Fix |
|---|---|---|
ValueError: mutable default <class 'list'> for field x is not allowed: use default_factory | list/dict/set (3.11+: any unhashable) default | field(default_factory=list) |
TypeError: non-default argument 'x' follows default argument (newer versions append the field name) | field order (often via inheritance) | reorder, defaults, kw_only |
FrozenInstanceError: cannot assign to field 'x' | assignment on frozen instance | replace(), or object.__setattr__ in __post_init__ |
TypeError: unhashable type: 'Cfg' | eq=True, not frozen | frozen=True, unsafe_hash=True or eq=False |
TypeError: unhashable type: 'list' from frozen hash() | list field in frozen class | tuple, or hash=False |
ValueError: The truth value of an array with more than one element is ambiguous | numpy/tensor field in __eq__ | field(compare=False) |
TypeError: cannot inherit frozen dataclass from a non-frozen one | mixed frozen inheritance | make both frozen or neither |
field x is declared with init=False, it cannot be specified with replace() (ValueError, TypeError on recent versions) | replace on init=False field | recompute in __post_init__ |
InitVar 'x' must be specified with replace() (ValueError/TypeError as above) | InitVar without default | pass it or give it a default |
TypeError: __init__() missing 1 required positional argument | field without default | default or pass the value |
AttributeError: 'X' object has no attribute 'y' in repr | init=False field never assigned | assign in __post_init__ or give a default |
TypeError: cannot pickle '_thread.lock' object from asdict | deep copy of non-copyable value | shallow to_dict() |
ValueError: 'x' in __slots__ conflicts with class variable | manual __slots__ with defaults | slots=True |
TypeError: Cannot overwrite attribute __lt__ in class X | order=True with hand-written ordering | remove one of them |
TypeError: super(type, obj): obj must be an instance or subtype of type | zero-arg super() in slots=True class (3.10-3.13; fixed in 3.14) | super(Cls, self) |
python-beginner-concept-tutor
Teach Python concepts to beginners heading into AI engineering (data scientists, analysts, students, career switchers): plain-language definitions, analogies, minimal examples, line-by-line walkthroughs, common mistakes, check-your-understanding questions and mini exercises; Socratic hints before solutions; explaining tracebacks; mutable vs immutable, references and aliasing, is vs ==, integer division, floating point, slicing and off-by-one, indentation errors; a staged learning path from variables to calling an LLM API, embeddings and a tiny RAG. Ships a script that lists the concepts a snippet uses in learning order and traces execution step by step like Python Tutor.
python-comprehension-refactorer
Refactor Python loops into list, dict and set comprehensions and generator expressions, and unroll comprehensions that are too clever: filtering, mapping, nested-loop flattening, conditional expressions, walrus in comprehensions, dict inversion, grouping with defaultdict/Counter, lazy generators for sum/any/all/max, streaming large files, itertools (chain, islice, groupby, batched, pairwise, accumulate, product), scoping rules, side-effect anti-patterns and performance (when map/filter or numpy/pandas vectorization win). AI-engineering focus: building prompt lists, batching texts for embeddings, filtering retrieved chunks by score, id->embedding dicts, flattening tool-call results, lazy dataset streaming and token counting.
python-context-manager-writer
Write correct Python context managers and find resource-handling bugs — __enter__/__exit__ semantics and exception suppression, contextlib (contextmanager, asynccontextmanager, ExitStack, AsyncExitStack, closing, aclosing, suppress, redirect_stdout, nullcontext, chdir), async with, re-entrancy, leaked files/connections/clients/pools/locks, and AI-engineering recipes: torch.no_grad/inference_mode/autocast, eval()/train() switches, GPU cache cleanup, token and latency meters, tracing spans, temp env vars, rate-limit semaphores, httpx/aiohttp client lifecycle, FastAPI lifespan, DB/vector-store transactions, atomic checkpoint writes, seeds and agent sandboxes.
python-exception-handling-designer
Design and review Python exception handling: custom exception hierarchies with retryable flags, what to catch where, EAFP vs LBYL, chaining with raise ... from, else/finally, logger.exception, add_note, ExceptionGroup and except* with asyncio.TaskGroup, warnings, asserts vs validation, FastAPI error translation, retries with backoff and jitter, timeouts, cancellation, pytest.raises, plus LLM-app failures: 429 rate limits with retry-after, 5xx overloaded, context-length exceeded, content filters, broken streams, malformed JSON repair loops, tool-call errors fed back to the model, partial embedding batch failures, CUDA OOM batch halving, vector DB outages, circuit breakers and model fallbacks.
python-idiomatic-code-reviewer
Review Python code for non-idiomatic, unpythonic or risky patterns and rewrite it into clean modern Python: PEP 8 / PEP 20, EAFP vs LBYL, iteration idioms (enumerate, zip, comprehensions, any/all), unpacking, f-strings, pathlib, truthiness, `is None`, mutable defaults, walrus, match statements, collections / itertools / functools, logging instead of print, with-blocks and resource handling. Includes AI-engineering checks: hardcoded API keys, time.sleep or requests inside async def, missing HTTP timeouts, unbounded retry loops, model clients built at import time, prompt string concatenation, sequential awaits, eval() on LLM output. Ships a 46-rule AST reviewer with severities and suggested rewrites.
python-type-hints-annotator
Add accurate, modern type hints to Python and fix mypy / pyright (pyrefly, ty) errors: builtin generics, X | None, TypeAlias and the 3.12 type statement, TypeVar and PEP 695 generics, Protocol, Callable / ParamSpec / Concatenate, TypedDict (Required, NotRequired, ReadOnly), Literal, Final, ClassVar, overload, Annotated, TypeGuard / TypeIs, Self, Never, TypeVarTuple, async types, Iterable vs Sequence vs Mapping, TYPE_CHECKING, stubs, py.typed, gradual strictness. AI-engineering focus: typed LLM message dicts and tool schemas, pydantic vs TypedDict, numpy NDArray, torch / jaxtyping shapes, streaming generators, async clients, callbacks, generic VectorStore protocols, typed config. Ships an AST audit: coverage gate, legacy-typing rewrites, inferred types.