Skip to main content
🧱
PEP 557 · Speedrun · Python 3.7 (2017)

PEP 557 in 90 seconds — @dataclass and the death of 50-line __init__

Annotations in, real class out. No boilerplate.

Before `@dataclass`, writing a simple data class was a litany of boilerplate: ```python class Point: def __init__(self, x, y=0): self.x = x self.y = y def __repr__(self): return f"Point(x={self.x}, y={self.y})" def __eq__(self, other): return isinstance(other, Point) and self.x == other.x and self.y == other.y ``` That's six lines saying "x is x, y is y". PEP 557 (Python 3.7, 2017) shipped `@dataclass` — write the annotations once, the decorator synthesises the rest.

`@dataclass` reads your class's annotations and generates `__init__`, `__repr__`, and `__eq__` automatically. For mutable defaults (lists, dicts), you use `field(default_factory=...)` so the default isn't shared across instances — the same trap as a mutable default argument. Extras: `frozen=True` makes instances immutable (`__hash__` synthesised too); `kw_only=True` (3.10+) forces keyword-only args; `slots=True` (3.10+) gives you `__slots__` for memory wins. PEP 681 generalises the whole pattern so Pydantic + SQLAlchemy v2 can opt in without inheriting from dataclass itself.

Before — class with 6-line boilerplate per field
class Cart:
    def __init__(self, user_id, items=None, discount=0.0):
        self.user_id = user_id
        self.items = items if items is not None else []  # mutable-default trap
        self.discount = discount

    def __repr__(self):
        return f"Cart(user_id={self.user_id}, items={self.items}, discount={self.discount})"

    def __eq__(self, other):
        if not isinstance(other, Cart):
            return NotImplemented
        return (self.user_id, self.items, self.discount) == (other.user_id, other.items, other.discount)
After — @dataclass synthesises everything
from dataclasses import dataclass, field

@dataclass
class Cart:
    user_id: int
    items: list = field(default_factory=list)   # safe mutable default
    discount: float = 0.0

# Synthesised methods: __init__, __repr__, __eq__.
# field(default_factory=list) avoids the shared-default trap.
# Optional: @dataclass(frozen=True) for immutability + __hash__.
# Optional: @dataclass(slots=True) on 3.10+ for __slots__ memory wins.

🎯 Predict the output

What does this print? `@dataclass` generates `__init__`, `__repr__`, and `__eq__` from the field annotations.

from dataclasses import dataclass, field

@dataclass
class Cart:
    user_id: int
    items: list = field(default_factory=list)
    discount: float = 0.0

c = Cart(user_id=42)
c.items.append("apple")
c.items.append("banana")
print(c)
print(c == Cart(user_id=42, items=["apple", "banana"]))
Classes & dataclasses → Foundations track

Or speedrun another PEP

PEP 572Assignment as an expression — 3 lines become 1.
PEP 572 in 90 seconds — the walrus that ate your for-loop
PEP 484Python stays dynamic — but you can opt into typing.
PEP 484 in 90 seconds — type hints, without TypeScript-envy
PEP 622Switch statements are a 5% feature. Pattern matching is the 95%.
PEP 622 in 90 seconds — match / case isn't just a switch