2.1.16 · D1OOP Fundamentals

Foundations — Dataclasses — `@dataclass` decorator, `__post_init__`

1,957 words9 min readBack to topic

The map of what you must already own

Before we touch a single symbol, here is how the pieces stack. Every arrow means "you cannot understand the target until you own the source."

class and object

self the current instance

dunder methods __init__ __repr__ __eq__

decorator the at symbol wrapper

type annotations name colon type

fields of a dataclass

default arguments

mutable default trap

dataclass topic

immutability and hashing

We now walk this map bottom-up. Each concept: plain words → the picture → why the topic needs it.


1. class and object — the blueprint and the thing

Picture. Look at the figure: on the left a paper blueprint labelled Point, on the right two physical stamped copies with real numbers filled in. The blueprint itself has no numbers — it's a form. Each object is one filled-in form.

Figure — Dataclasses — `@dataclass` decorator, `__post_init__`

Why the topic needs it. @dataclass sits on top of a class. If you don't see the blueprint-vs-copy split, the sentence "the decorator generates __init__" has nothing to attach to. The whole topic is "make the blueprint auto-write its own machinery."


2. self — the word "this particular copy"

Picture. In the figure above, the cyan arrow labelled self points from the method back into one of the two filled forms — never both. self.x reads the x slot of exactly the copy the arrow touches.

Why the topic needs it. The generated __init__ is literally a pile of self.field = field lines (parent Step 3). Every [!formula] in the parent — "assign every field: self.f = f" — is self doing its job.


3. Dunder methods — the double-underscore hooks

The three the topic cares about:

You write Python secretly calls Its job
Point(1, 2) __init__ build & fill the object
print(p) / repr(p) __repr__ make a readable text form
p == q __eq__ decide if two objects are "equal"

Picture. The figure shows a "vending machine" panel: pressing the outer buttons (Point(...), print, ==) trips an inner lever (__init__, __repr__, __eq__). You touch the outside; the dunder fires inside.

Figure — Dataclasses — `@dataclass` decorator, `__post_init__`

4. Decorators — the @ wrapper

So this:

@dataclass
class Point:
    ...

means exactly Point = dataclass(Point). Python builds the plain class first, then feeds it to dataclass, and rebinds the name Point to whatever comes back.

Picture. The figure shows a bare class entering a machine stamped @dataclass; the same class exits with three new dunder plates bolted on. Same object, more equipment.

Figure — Dataclasses — `@dataclass` decorator, `__post_init__`

5. Type annotations — name: type

Picture. The figure shows a form blank with a small tag hanging off it reading : float. The tag doesn't stop you writing anything in the blank — it just declares intent.

Figure — Dataclasses — `@dataclass` decorator, `__post_init__`

6. __annotations__ and "field" — the ordered dictionary

Why order matters. The generated __init__(self, x, y, label="P") lists parameters in the same order the fields appear. Reorder the class body and you reorder the constructor — so order is not cosmetic.


7. Default arguments — pre-filled blanks

Picture. The figure shows the form with the label blank pre-printed with "P" in faint amber — you can overwrite it, or leave it.

Rule the topic relies on: once one field has a default, every field after it must also have one — otherwise a defaulted parameter would sit before a required one, which Python forbids. Background: Default Arguments and Mutable Default Trap.


8. The mutable-default trap — why = [] is banned

Picture. The figure shows three objects (three forms) with arrows all pointing to a single shared list box — versus the fixed version where each form has arrows to its own box.


9. Immutability and hashing — the frozen door


10. Sibling shapes — so you don't confuse them

The parent lives in a family of "data holder" tools. You don't need them for this page, but knowing they exist prevents mix-ups: NamedTuple vs Dataclass vs TypedDict compares the three. A NamedTuple is immutable and tuple-like; a TypedDict is just a dict with annotated keys; a dataclass is a real, mutable-by-default class.

All of this feeds the parent topic: Dataclasses & `__post_init__`, and its OOP home OOP Fundamentals.


Equipment checklist

Cover the right side; can you answer each before revealing?

A class is a ___ and an object is a ___
A blueprint / an actual thing built from it (an instance).
Inside a method, self refers to ___
The specific object the method was called on.
p == q secretly calls which dunder?
__eq__.
On a plain class, what does the default __eq__ compare?
Identity — the memory address, not the field values.
@dataclass above a class is shorthand for ___
Point = dataclass(Point) — feed the class into the decorator, rebind the name.
Why must dataclass fields be annotated with name: type?
The decorator finds fields by reading __annotations__; un-annotated x = 5 is invisible to it.
Does a type annotation enforce the type at runtime?
No — it's only a signal/label; nothing checks or converts.
Fields are stored in __annotations__ in what order?
Source order — which fixes the __init__ parameter order.
Once a field has a default, what's true of every later field?
It must also have a default.
Why is items: list = [] banned in a dataclass?
A single default list would be shared across all instances; use field(default_factory=list) for a fresh one each time.
Why can only frozen=True dataclasses be dict keys / set members?
Their data can't change, so their hash stays stable and Python generates __hash__; mutable ones set __hash__ = None.