2.1.16OOP Fundamentals

Dataclasses — `@dataclass` decorator, `__post_init__`

1,818 words8 min readdifficulty · medium

WHAT is a dataclass?

from dataclasses import dataclass
 
@dataclass
class Point:
    x: float          # field (must be annotated!)
    y: float
    label: str = "P"  # field with default

This 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
Figure — Dataclasses — `@dataclass` decorator, `__post_init__`

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?
The decorator discovers fields by reading __annotations__; an un-annotated x = 5 is just a class attribute, not a field.
When is __post_init__ called?
As the last statement of the auto-generated __init__, after all fields are assigned.
Why is items: list = [] an error in a dataclass?
One mutable list would be shared across all instances; Python raises ValueError. Use field(default_factory=list).
What does field(init=False) do?
Removes the field from __init__'s parameters; typically set later in __post_init__.
Why does frozen=True make a dataclass hashable?
Fields can't change after creation, so the hash stays stable; Python generates __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?
It compares the tuple of all fields field-by-field (and requires same class).
What's default_factory and why use it over default?
A zero-arg callable invoked once per instance for mutable defaults, giving each object its own object.

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 Hashingfrozen=True and __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

eliminates

reads

expressed via

drives generation of

includes

includes

includes

class value becomes

used by

shared list causes

fixed by

fresh per instance in

Boilerplate init repr eq

@dataclass decorator

Type-annotated fields

Declare what data you hold

Generated dunders

__init__

__repr__

__eq__

Field defaults

Mutable default trap

field(default_factory=list)

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.

Go deeper — visual, from zero

Test yourself — OOP Fundamentals