Dataclasses — `@dataclass` decorator, `__post_init__`
WHAT is a dataclass?
from dataclasses import dataclass
@dataclass
class Point:
x: float # field (must be annotated!)
y: float
label: str = "P" # field with defaultThis single declaration is roughly equivalent to hand-writing:
class Point:
def __init__(self, x: float, y: float, label: str = "P"):
self.x = x
self.y = y
self.label = label
def __repr__(self):
return f"Point(x={self.x!r}, y={self.y!r}, label={self.label!r})"
def __eq__(self, other):
if other.__class__ is not self.__class__:
return NotImplemented
return (self.x, self.y, self.label) == (other.x, other.y, other.label)HOW the decorator works (derivation from scratch)
Let's build the mental model step by step — Feynman-style — so the magic disappears.
Step 1 — Collect fields. Python reads cls.__annotations__, an ordered dict {name: type}. Order = source order. Why? __init__'s parameter order must match field order.
Step 2 — Find defaults. For each annotated name, check if a class-level value exists (label = "P"). If so it's the default. Why? So __init__(... label="P") can be generated.
Step 3 — Generate __init__. Build a function string: parameters in field order, defaults attached, body = self.<name> = <name>. Why a generated function? Speed + correctness vs reflection at runtime.
Step 4 — Generate __repr__ / __eq__ unless you disabled them (@dataclass(repr=False)). __eq__ compares the tuple of fields. Why a tuple? Tuple comparison gives lexicographic, field-by-field equality for free.
__post_init__ — running code AFTER auto-__init__
from dataclasses import dataclass, field
@dataclass
class Rectangle:
width: float
height: float
area: float = field(init=False) # NOT a constructor arg; we compute it
def __post_init__(self):
if self.width <= 0 or self.height <= 0:
raise ValueError("dimensions must be positive")
self.area = self.width * self.height
Worked examples
Recall Feynman: explain to a 12-year-old
Imagine you fill out the same form every time you make a new toy: name, color, size. @dataclass is a robot stamp — you just list the blanks (name, color, size) and the robot writes the whole form, plus a way to read it out loud and a way to check if two toys are the same. __post_init__ is a "now double-check" step: after the robot fills the form, it runs your little checklist ("size can't be zero!") and can compute extra stuff ("total price = price × quantity").
Flashcards
What does the @dataclass decorator auto-generate from annotated fields?
__init__, __repr__, and __eq__ (plus __hash__ if frozen).Why must dataclass fields be type-annotated?
__annotations__; an un-annotated x = 5 is just a class attribute, not a field.When is __post_init__ called?
__init__, after all fields are assigned.Why is items: list = [] an error in a dataclass?
field(default_factory=list).What does field(init=False) do?
__init__'s parameters; typically set later in __post_init__.Why does frozen=True make a dataclass hashable?
__hash__. Mutable dataclasses set __hash__ = None.How do you assign a derived field inside __post_init__ of a frozen dataclass?
object.__setattr__(self, "name", value) to bypass the freeze.How does the generated __eq__ compare two instances?
What's default_factory and why use it over default?
Connections
- OOP Fundamentals — dataclasses are a shortcut for the data-holding pattern of classes.
- Dunder Methods __init__ __repr__ __eq__ — what the decorator actually generates.
- Immutability and Hashing —
frozen=Trueand__hash__. - Default Arguments and Mutable Default Trap — same bug, different syntax.
- NamedTuple vs Dataclass vs TypedDict — when to pick each.
- Type Annotations in Python — why annotations drive field detection.
Concept Map
Hinglish (regional understanding)
Intuition Hinglish mein samjho
Dekho, jab bhi hum ek simple "data rakhne wali" class banate hain, har baar wahi boring kaam karna padta hai: __init__ mein self.x = x likho, __repr__ likho taaki print karne par theek dikhe, aur __eq__ likho taaki do objects value se compare ho. @dataclass ek code-generator hai jo ye saara boilerplate khud likh deta hai — aapke type-annotated fields padh kar. Bas aap likho x: float, aur Python __init__, __repr__, __eq__ bana deta hai. Yaad rakho: annotation zaroori hai, warna woh field gina hi nahi jaata.
Ek bada trap: mutable default. items: list = [] likhoge to error aayega, kyunki ek hi list saare objects mein share ho jaati. Solution hai field(default_factory=list) — ye har object ke liye nayi list banata hai. Isi tarah field(init=False) un fields ke liye hai jo caller nahi deta, balki hum khud compute karte hain (jaise area).
Ab __post_init__ ka kaam samjho. Auto-__init__ sirf assignment karta hai — koi validation ya derived calculation nahi. To jaha aap chaho ki "construction ke turant baad" check karna ho ya extra field nikaalni ho (jaise area = width * height), waha __post_init__ likho. Ye method auto-__init__ ke bilkul end mein call hota hai, jab saari fields set ho chuki hoti hain. Frozen dataclass (frozen=True) immutable bana deta hai aur hashable bhi — isliye woh dict key ya set mein use ho sakta hai. Yeh sab samajhne se aapka code chhota, saaf aur bug-free ho jaata hai.