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: ...
ParameterDefaultEffect
initTruegenerate __init__
reprTruegenerate __repr__
eqTruegenerate __eq__, compares the field tuple
orderFalsegenerate __lt__, __le__, __gt__, __ge__
unsafe_hashFalseforce __hash__ even on a mutable class
frozenFalseblock attribute assignment, and make it hashable
match_argsTruegenerate __match_args__ for positional patterns
kw_only (3.10)Falseevery field becomes keyword-only
slots (3.10)Falserebuild the class with __slots__
weakref_slot (3.11)Falseadd __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() argumentMeaning
default=plain default, same as x: int = 5
default_factory=zero-arg callable called per instance, for mutables
init=Falsekeep it out of __init__, set it in __post_init__
repr=Falsehide from __repr__, use it for secrets and blobs
compare=Falseexclude 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:

FormBytes per instance
default (__dict__)344 (56 object + 288 dict)
slots=True48

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.

NeedUse
mutable record, methods, stdlib only@dataclass
immutable value object, hashable@dataclass(frozen=True, slots=True)
tuple semantics, unpacking, indexingNamedTuple
just a shape for an existing dictTypedDict
runtime validation and coercion of external datapydantic.BaseModel
converters, validators, stdlib-free flexibilityattrs
heavy behavior, custom __init__a plain class
dataclassNamedTupleTypedDictpydanticattrs
runtime type validationnononoyesopt-in
mutableyesnoyesyesyes
is a dictnonoyesnono
indexable / unpackablenoyesnonono
stdlibyesyesyesnono
inheritanceyeslimitedyesyesyes
slots support3.10+inherentn/ayesyes
JSON in and outasdict_asdictnativemodel_dumpasdict

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.