Dataclasses and type hints
Describe records and communicate expected types
Typed records
dataclass generates methods such as initialization, representation, and equality from annotated fields. It remains a normal class and can define custom methods.
Type hints describe expected types for readers and static tools. They do not parse, convert, or generally validate values at runtime. str | None, list[int], and Callable describe optional values, element types, and callbacks.
from dataclasses import dataclass, field
@dataclass
class Reading:
source: str
values: list[float] = field(default_factory=list)
left = Reading("sensor-a")
right = Reading("sensor-b")
left.values.append(2.5)
print(left.values, right.values) # [2.5] []default_factory=list creates independent storage for each instance. The annotation does not validate later calls to append. Generated equality compares fields for instances of the same dataclass type; the generated representation names the class and fields.
Use a dictionary for dynamic keys, a dataclass for a stable record shape, or a regular class when state transitions dominate.
Defaults and validation
Put required fields before defaulted fields in an ordinary generated initializer. Use field(default_factory=...) for mutable per-instance defaults.
__post_init__ can enforce a construction rule, but later mutations may bypass it. Decide whether validation belongs at the parser, constructor, or public API boundary.
Gotchas
frozen=True blocks ordinary field reassignment, but does not freeze referenced objects:
from dataclasses import dataclass
@dataclass(frozen=True)
class Snapshot:
labels: list[str]
snapshot = Snapshot(["stable"])
snapshot.labels.append("changed")
print(snapshot.labels) # ['stable', 'changed']
try:
hash(snapshot)
except TypeError:
print("list field is unhashable")The list changes without rebinding the field. A frozen dataclass with equality normally gets a generated hash, but computing it requires hashable field values. Use immutable values for stable keys; unsafe_hash=True does not make mutable identity safe.
dataclasses.replace normally reuses unchanged field objects. A default factory controls new-instance defaults, not deep copying. Static checking, runtime validation, and behavior tests serve different purposes.
Put it into practice
Implement the exercise in a full-screen workspace, then run its tests.