Python Cheatsheet
Dataclasses
Use this Python reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
@dataclass Basics
The decorator reads the class-level annotations and writes the boilerplate for
you. Only annotated class attributes become fields, x = 5 with no annotation
is just a class attribute.
from dataclasses import dataclass @dataclass class Point: x: int y: int = 0 # defaults must follow non-defaults p = Point(1, 2) p # Point(x=1, y=2) generated __repr__ p == Point(1, 2) # True generated __eq__ p.x = 5 # mutable by default Point.__match_args__ # ('x', 'y') so `case Point(1, y)` works
Generated for free: __init__, __repr__, __eq__, and __match_args__.
Not generated unless you ask: ordering, hashing, immutability, slots.
Decorator Parameters
@dataclass(frozen=True, slots=True, kw_only=True, order=True) class Config: ...
| Parameter | Default | Effect |
|---|---|---|
init | True | generate __init__ |
repr | True | generate __repr__ |
eq | True | generate __eq__, compares the field tuple |
order | False | generate __lt__, __le__, __gt__, __ge__ |
unsafe_hash | False | force __hash__ even on a mutable class |
frozen | False | block attribute assignment, and make it hashable |
match_args | True | generate __match_args__ for positional patterns |
kw_only (3.10) | False | every field becomes keyword-only |
slots (3.10) | False | rebuild the class with __slots__ |
weakref_slot (3.11) | False | add __weakref__ when using slots=True |
Hashing rules that surprise people: eq=True (the default) sets
__hash__ = None, so a plain dataclass is unhashable and cannot go in a set
or be a dict key. frozen=True, eq=True generates a real __hash__. Use
unsafe_hash=True only when you promise not to mutate the compared fields.
order=True compares the tuple of fields in declaration order, so field order is
your sort key.
field()
Reach for field() when a plain default is not enough.
from dataclasses import dataclass, field @dataclass class Item: name: str tags: list[str] = field(default_factory=list) # fresh list per instance lookup: dict[str, int] = field(default_factory=dict) total: float = field(init=False, default=0.0) # set in __post_init__ token: str = field(default="", repr=False, compare=False) id: int = field(default=0, metadata={"unit": "px", "help": "row id"}) admin: bool = field(default=False, kw_only=True) # 3.10, this field only
field() argument | Meaning |
|---|---|
default= | plain default, same as x: int = 5 |
default_factory= | zero-arg callable called per instance, for mutables |
init=False | keep it out of __init__, set it in __post_init__ |
repr=False | hide from __repr__, use it for secrets and blobs |
compare=False | exclude from __eq__ and ordering |
hash= | override the compare-derived hash inclusion |
kw_only= (3.10) | make just this field keyword-only |
metadata= | read-only mapping for other tools, ignored by dataclasses |
A mutable default is a hard error, not a silent shared-state bug:
@dataclass class Bad: items: list = [] # ValueError: mutable default <class 'list'> for field items is not allowed: # use default_factory
The check covers list, dict, and set (anything unhashable). A mutable
default on a custom class slips through, so use default_factory for any
object you intend to mutate.
__post_init__ and InitVar
__post_init__ runs at the end of the generated __init__. InitVar[T] declares
a constructor-only argument, it is passed to __post_init__ and never stored as
a field.
from dataclasses import dataclass, field, InitVar @dataclass class Rect: w: int h: int area: int = field(init=False) scale: InitVar[int] = 1 def __post_init__(self, scale: int) -> None: self.area = self.w * self.h * scale r = Rect(2, 3, scale=2) # Rect(w=2, h=3, area=12) r.scale # AttributeError, InitVar is not a field
Validation goes here too, since dataclasses do not validate anything themselves.
On a frozen=True class, __post_init__ must use
object.__setattr__(self, "area", ...).
Helper Functions
from dataclasses import asdict, astuple, replace, fields, is_dataclass, MISSING asdict(p) # {'x': 1, 'y': 2}, RECURSES into nested dataclasses astuple(p) # (1, 2), also recursive replace(p, y=9) # Point(x=1, y=9), a new instance, runs __init__ fields(p) # tuple of Field objects [f.name for f in fields(p)] # ['x', 'y'] fields(p)[0].type # <class 'int'>, a STRING under lazy annotations fields(p)[5].metadata # mappingproxy({'unit': 'px', ...}) is_dataclass(p) # True for a class OR an instance is_dataclass(p) and not isinstance(p, type) # True only for an instance
asdict deep-copies with copy.deepcopy, which is slow on large trees and blows
up on unpicklable members. For a shallow one use
{f.name: getattr(o, f.name) for f in fields(o)}. replace calls __init__,
so init=False fields cannot be passed to it.
Inheritance and the Default Ordering Problem
Fields are collected base first, so a base class default poisons every subclass that adds a required field.
@dataclass class Base: a: int = 0 @dataclass class Child(Base): b: int # TypeError: non-default argument 'b' follows default # argument 'a'
Three fixes, in order of preference:
# 1. kw_only makes ordering irrelevant (3.10+), the usual answer @dataclass(kw_only=True) class Base: a: int = 0 @dataclass(kw_only=True) class Child(Base): b: int Child(b=1) # Child(a=0, b=1) # 2. KW_ONLY sentinel, everything after it is keyword-only from dataclasses import KW_ONLY @dataclass class Mixed: a: int _: KW_ONLY b: int = 2 # 3. Give the subclass field a default too
A subclass redeclaring a field keeps the base position and takes the new default.
Frozen and slots
@dataclass(frozen=True, slots=True) class Money: amount: int currency: str = "USD" m = Money(10) m.amount = 20 # dataclasses.FrozenInstanceError {m} # fine, frozen dataclasses are hashable object.__setattr__(m, "amount", 20) # escape hatch, needed in __post_init__ m.spam = 1 # AttributeError, no __dict__ under slots
slots=True replaces the instance __dict__ with fixed descriptors. Measured on
a two-int dataclass with sys.getsizeof:
| Form | Bytes per instance |
|---|---|
default (__dict__) | 344 (56 object + 288 dict) |
slots=True | 48 |
Costs: no new attributes at runtime, no default weakref support (pass
weakref_slot=True), and slots=True returns a new class object, so a
decorator or reference captured before it will point at the old one.
Pick the Right Container
Dataclasses do not validate types at runtime. Point("a", "b") constructs
happily. If untrusted input crosses the boundary, use pydantic.
| Need | Use |
|---|---|
| mutable record, methods, stdlib only | @dataclass |
| immutable value object, hashable | @dataclass(frozen=True, slots=True) |
| tuple semantics, unpacking, indexing | NamedTuple |
just a shape for an existing dict | TypedDict |
| runtime validation and coercion of external data | pydantic.BaseModel |
| converters, validators, stdlib-free flexibility | attrs |
heavy behavior, custom __init__ | a plain class |
| dataclass | NamedTuple | TypedDict | pydantic | attrs | |
|---|---|---|---|---|---|
| runtime type validation | no | no | no | yes | opt-in |
| mutable | yes | no | yes | yes | yes |
is a dict | no | no | yes | no | no |
| indexable / unpackable | no | yes | no | no | no |
| stdlib | yes | yes | yes | no | no |
| inheritance | yes | limited | yes | yes | yes |
slots support | 3.10+ | inherent | n/a | yes | yes |
| JSON in and out | asdict | _asdict | native | model_dump | asdict |
pydantic.BaseModel reads the same annotations and coerces or rejects on
construction, and pydantic.dataclasses.dataclass is a drop-in that adds
validation to the stdlib decorator.