Foundations — Dataclasses — `@dataclass` decorator, `__post_init__`
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."
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.

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.

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.

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.

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 ___
Inside a method, self refers to ___
p == q secretly calls which dunder?
__eq__.On a plain class, what does the default __eq__ compare?
@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?
__annotations__; un-annotated x = 5 is invisible to it.Does a type annotation enforce the type at runtime?
Fields are stored in __annotations__ in what order?
__init__ parameter order.Once a field has a default, what's true of every later field?
Why is items: list = [] banned in a dataclass?
field(default_factory=list) for a fresh one each time.Why can only frozen=True dataclasses be dict keys / set members?
__hash__; mutable ones set __hash__ = None.