Skip to main content

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:

BuilderSinceInstancesClass syntax
collections.namedtuple2.6Immutable (tuple)Factory call
typing.NamedTuple3.5 / class body 3.6Immutable (tuple)Yes, with annotations
@dataclasses.dataclass3.7Mutable by default; frozen=TrueDecorator, 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​

Quiz

Data class builders

1 / 2

collections.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​

Quiz

Type hints

1 / 2

Pair(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​

Quiz

@dataclass

1 / 2

guests: 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​

Quiz

Smell and match

1 / 2

case 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 writeIt becomes
n: intinstance field (required in __init__)
n: int = 99instance field (optional, default 99)
n = 99class attribute (not a field, no __init__ arg)
n: ClassVar[int] = 99class 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​

Quiz

Instance vs class on a dataclass

1 / 3

On @dataclass class Spam, which of these are class attributes (not fields)?

repeat: int
repeat: int = 99
repeat = 99
repeat: 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​

Key takeaways
  1. namedtuple / NamedTuple / @dataclass generate the boring special methods.
  2. Tuple builders are immutable; dataclass is mutable unless frozen=True.
  3. Type hints are not runtime checks.
  4. Mutable field defaults need field(default_factory=...).
  5. _asdict / asdict and _replace / replace clone with edits.
  6. Data-only types can be a design smell; class patterns use __match_args__ or keywords.
  7. On a dataclass, a type hint makes an instance field; a bare assignment (or ClassVar) stays on the class.

Exercises​

Activity: Replace a field

namedtuple("Pt", "x y"). Read x y nx. Return _replace(x=nx) and print the new x and y.