Data Class Builders
Source: Fluent Python, 2nd Edition — Chapter 5 by Luciano Ramalho
A data class is a type that is mostly fields: a record with a decent __repr__ and __eq__, not a 20-line __init__. Python gives you three shortcuts:
| Builder | Since | Instances | Class syntax |
|---|---|---|---|
collections.namedtuple | 2.6 | Immutable (tuple) | Factory call |
typing.NamedTuple | 3.5 / class body 3.6 | Immutable (tuple) | Yes, with annotations |
@dataclasses.dataclass | 3.7 | Mutable by default; frozen=True | Decorator, no extra base |
TypedDict only annotates mappings; it does not build instantiable classes. Skip it here.
None of these require you to inherit a special business base. namedtuple / NamedTuple produce tuple subclasses. @dataclass injects methods; your class still subclasses object unless you say otherwise.
Why not a hand-written bag of attributes?
A plain class with only __init__ has a useless repr and == that compares identity, not field values. The builders generate __init__, __repr__, and __eq__ (and more, if you ask).
🧪 namedtuple vs homemade
Mini quiz — the three builders
Data class builders
1 / 2collections.namedtuple instances are
Pick an answer to see if you are right.
Challenge: named point
Build Pt = namedtuple("Pt", "x y"). Read two integers. Print x+y using named fields (not p[0]).
namedtuple extras
You can index as well as use names. _fields lists names. _make(iterable) is like Cls(*row). _asdict() is a regular dict (insertion-ordered). _replace(**kwargs) returns a new instance. defaults= fills the rightmost fields.
Injecting a method by assignment (Cls.f = fn) works but is a last resort — prefer a class body (NamedTuple or @dataclass).
typing.NamedTuple
Same tuple core, plus annotations:
from typing import NamedTuple
class Coordinate(NamedTuple):
lat: float
lon: float
reference: str = "WGS84"
NamedTuple in the bases list is not a real superclass you issubclass against; the result is a tuple subclass. Type hints are not checked when the program runs. A checker (Mypy, your IDE) reads them at rest.
A bare x: int on a plain class only fills __annotations__. NamedTuple / @dataclass turn annotated names into constructor fields. A name without an annotation (c = "spam") stays a class attribute, not a field.
Prefer typing.get_type_hints(cls) over reading __annotations__ yourself.
🧪 hints are documentation
Mini quiz — hints and NamedTuple
Type hints
1 / 2Pair(a: int) still accepts Pair("x") at runtime
Pick an answer to see if you are right.
Challenge: as dict
typing.NamedTuple with name: str and n: int. Read word count. Print name=... n=... from _asdict() items in that order.
@dataclass
from dataclasses import dataclass, field, asdict, replace
@dataclass
class Player:
name: str
score: int = 0
Useful decorator flags: frozen=True (assign raises FrozenInstanceError), order=True (sort by fields). eq and frozen together make instances hashable.
Never write guests: list = []. @dataclass rejects list/dict/set literals as defaults. Use:
tags: list = field(default_factory=list)
__post_init__ runs at the end of the generated __init__ (validation, derived fields). InitVar is a constructor-only argument; ClassVar keeps a name off the instance fields.
dataclasses.asdict / replace mirror _asdict / _replace. fields(cls) lists Field objects.
A long record (Dublin Core-style) is a typical @dataclass: many optional fields, field(default_factory=list), maybe a custom __repr__.
🧪 default_factory
Mini quiz — dataclass
@dataclass
1 / 2guests: list = [] on a dataclass
Pick an answer to see if you are right.
Challenge: own list each time
@dataclass with name: str and nums: list via default_factory. Read a name, then integers. Append them and print len of .nums.
Code smell, then match
A class with only getters for data and no behavior can mean the real logic is scattered. That is Fowler’s data class smell — not a ban on namedtuple. Fine uses: throwaway scripts, import/export records you treat as immutable at the boundary, or scaffolding you later grow methods onto.
match can destructure any instance. case float(): is special for a few builtins. case float: (no call) accidentally matches everything and binds the name float.
Keyword: case City(continent="Asia", country=cc):. Positional uses __match_args__ (the builders fill it): case City("Asia", _, country):.
🧪 class pattern
Mini quiz — smell and match
Smell and match
1 / 2case float: # no parentheses
Pick an answer to see if you are right.
Challenge: match continent
NamedTuple City with continent and name. Read continent name. Print asia if continent is Asia, else other. Use match.
Instance fields vs class attributes
@dataclass needs a type hint on every instance field. That sits oddly next to PEP 484’s “hints are optional.” If you want records without annotations, look at attrs (or a plain namedtuple).
The class body also flips an old Python rule. Without a decorator, a name bound at class level is a class attribute. With @dataclass:
| You write | It becomes |
|---|---|
n: int | instance field (required in __init__) |
n: int = 99 | instance field (optional, default 99) |
n = 99 | class attribute (not a field, no __init__ arg) |
n: ClassVar[int] = 99 | class attribute with a type (the escape hatch) |
ClassVar is the exception to the exception: “annotated means instance” unless the type is this marker. That is easy to get wrong. Read the constructor signature (Help / __doc__) when you are unsure.
🧪 Same name, three spellings
Mini quiz — the annotation trap
Instance vs class on a dataclass
1 / 3On @dataclass class Spam, which of these are class attributes (not fields)?
repeat: intrepeat: int = 99repeat = 99repeat: ClassVar[int] = 99
Pick an answer to see if you are right.
Challenge: factory counter
Read integers. Build one dataclass instance per number. The count of instances must live on the class (so Box(n) still takes only n). Print the last n and how many boxes exist.
Chapter summary
namedtuple/NamedTuple/@dataclassgenerate the boring special methods.- Tuple builders are immutable; dataclass is mutable unless
frozen=True. - Type hints are not runtime checks.
- Mutable field defaults need
field(default_factory=...). _asdict/asdictand_replace/replaceclone with edits.- Data-only types can be a design smell; class patterns use
__match_args__or keywords. - On a dataclass, a type hint makes an instance field; a bare assignment (or
ClassVar) stays on the class.
Exercises
namedtuple("Pt", "x y"). Read x y nx. Return _replace(x=nx) and print the new x and y.