~22 min · dataclass, frozen, kw_only, field, post_init
Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
The promise — three lines instead of thirty
Most simple classes need __init__, __repr__, __eq__ — and writing them by hand is repetitive. @dataclass reads the class body's type hints and generates all of those. class Point: x: int; y: int — three lines of declaration, behaving exactly like the verbose hand-written version.
The decorator parameters that actually matter
frozen=True makes instances immutable (no attribute assignment after construction; auto-implements __hash__). kw_only=True makes every field keyword-only at construction (no positional arguments). order=True generates ordering dunders. slots=True (3.10+) uses __slots__ for memory efficiency. The defaults (@dataclass with no parens) are usually right for one-off data containers.
field() — for the cases the simple form can't express
Default values for fields work the same way as default arguments — and have the same mutable-default trap. tags: list[str] = [] shares the list across instances. The fix: tags: list[str] = field(default_factory=list). field() also lets you skip a field in __repr__, exclude it from __eq__, or mark it init=False.
__post_init__ — extra setup after auto-init
Dataclass generates __init__, but sometimes you need extra setup logic — validation, derived fields, etc. Define __post_init__ and it runs at the end of the auto-generated __init__. This is where you put "compute the area from the radius" or "raise if the value is negative."
Pythonic Way: Reach for @dataclass as the default for any value-shaped class — request bodies, config records, parsed events. Reach for plain class when you have meaningful behavior beyond "hold these fields". Reach for typing.NamedTuple when you specifically want tuple semantics (immutable, indexable, unpackable).
Code
Hand-written class vs dataclass — same behavior·python
from dataclasses import dataclass
# Hand-written
class PointManual:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"PointManual(x={self.x}, y={self.y})"
def __eq__(self, other):
return isinstance(other, PointManual) and self.x == other.x and self.y == other.y
# Dataclass
@dataclass
class Point:
x: int
y: int
p1 = Point(3, 4)
p2 = Point(3, 4)
print(p1) # Point(x=3, y=4)
print(p1 == p2) # True
frozen, kw_only, order — the useful flags·python
from dataclasses import dataclass
@dataclass(frozen=True)
class Coordinate:
lat: float
lon: float
c = Coordinate(37.5, 127.0)
try:
c.lat = 40.0 # frozen — assignment raises
except Exception as e:
print(type(e).__name__, e)
# frozen also gives __hash__ for free
print({c, Coordinate(37.5, 127.0)}) # set with one element (dedup'd)
@dataclass(kw_only=True)
class Config:
host: str
port: int = 8000
debug: bool = False
# Must use keyword arguments
cfg = Config(host="localhost", debug=True)
try:
Config("localhost", 9000) # positional rejected
except TypeError as e:
print(e)
field — defaults, factories, exclusions·python
from dataclasses import dataclass, field
@dataclass
class User:
name: str
tags: list[str] = field(default_factory=list) # fresh list each instance
password_hash: str = field(repr=False, default="") # excluded from __repr__
id: int = field(init=False, default=0) # not in __init__ args
u1 = User("alice")
u1.tags.append("admin")
u2 = User("bob")
print(u1.tags) # ['admin']
print(u2.tags) # [] — independent
print(u1) # User(name='alice', tags=['admin'], id=0) — password_hash hidden
__post_init__ — derived fields and validation·python
from dataclasses import dataclass, field
@dataclass
class Range:
start: int
end: int
length: int = field(init=False) # not in __init__, computed
def __post_init__(self):
if self.end < self.start:
raise ValueError("end must be >= start")
self.length = self.end - self.start
r = Range(start=10, end=25)
print(r) # Range(start=10, end=25, length=15)
try:
Range(start=10, end=5)
except ValueError as e:
print(e)
When NOT to dataclass — when you have real behavior·python
# A dataclass is for VALUES (data with auto __init__/__repr__/__eq__).
# When the class has rich behavior, write a plain class.
class Connection: # behavior-heavy — not a great dataclass
def __init__(self, host):
self.host = host
self._socket = None
def connect(self):
# complex setup
self._socket = f"<socket to {self.host}>"
def send(self, data):
if self._socket is None:
raise RuntimeError("not connected")
return f"sent {data!r} via {self._socket}"
# Trying to dataclass this would force you to use post_init for setup
# and add a bunch of field(init=False) — clearer to write the class directly.
Build a @dataclassOrder with fields id: str, items: list[str], total: float = 0.0. Use field(default_factory=list) for items. Add a __post_init__ that recomputes total from a price lookup dict you pass in (hint: store the price dict as a class-level constant on the dataclass — PRICES: ClassVar[dict] = {...}). Then make a second variant FrozenOrder(frozen=True, kw_only=True) and demonstrate immutability + that you can put it in a set.
Progress
Progress is local-only — sign in to sync across devices.