Document expected types and bundle fields into a record class without boilerplate.
Type hints are labels on jars (‘flour, not salt’); dataclasses are pre-printed name-tag stickers you slap on a box.
def parse(s: str) -> list[int]: @dataclass
... class User:
name: str
# runtime: duck-typed age: int = 0
# mypy / pyright catch mismatches @dataclass(frozen=True)
before runtime class Id: value: int
Type hints (PEP 484) live in __annotations__ and inspect.signature; they do NOT change runtime behaviour - they feed static checkers (mypy/pyright/ruff). The payoff: mismatches are caught BEFORE the code runs. @dataclass (PEP 557) auto-generates __init__, __repr__, __eq__. frozen=True makes immutable + hashable; slots=True saves memory by skipping __dict__.
def greet(name: str, times: int = 1) -> list[str]:
return [f'Hello {name}'] * times
annotations are doc that mypy/pyright verify.
from dataclasses import dataclass, field
@dataclass
class Point:
x: float
y: float
tags: list[str] = field(default_factory=list)
field(default_factory=list) ensures each instance gets a fresh list.
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class Money:
amount_cents: int
currency: str
# m.amount_cents = 2000 -> FrozenInstanceError
frozen=True makes hashable + immutable; slots=True saves ~50% memory.
Confusing types with enforcement. Hints are NOT enforced at runtime - use pydantic or beartype for untrusted input. Falling for mutable defaults (tags: list[str] = []) - every instance shares the same list.
Annotate every public function; @dataclass for data records; frozen=True for value objects; slots=True for tight loops; remember hints are doc, not enforcement.
Order.line_items lists shared one default list; orders bled items.tags: list[str] = [] evaluated once at class-def time, shared.field(default_factory=list).None because endpoint assumed dict[str, int].