When would you choose typing.NamedTuple over a dataclass for a record?
answer
- Value versus entity is the real question
- One of them still behaves like a sequence
- Mutation, validation and inheritance argue one way
- Hashable key and unpacking argue the other
- typing.NamedTuple forbids extra base classes
basics
~20 sChoose typing.NamedTuple when the record is a small immutable value that should keep tuple behaviour — unpacking, indexing, hashing, sorting. Choose a dataclass when you need mutation, validation, inheritance, or a record that must not act like a sequence.
solid answer
~40 s`typing.NamedTuple` is the class form of `collections.namedtuple`: annotated fields, defaults, methods and a docstring in a class body, but the result is still a `tuple` subclass. That buys immutability and hashability for free, plus unpacking, indexing, element-wise comparison and compatibility with any sequence-shaped API. A dataclass buys the opposite set: mutable by default (or `frozen=True` when you want otherwise), a `slots=True` option, per-field defaults produced by a factory, free inheritance, a post-initialisation hook for validation, and equality scoped to the same class so two unrelated record types never compare equal. `typing.NamedTuple` also has hard limits — defaults must be rightmost, and you cannot mix it with another base class. Rule of thumb: value that travels and gets used as a key, `NamedTuple`; entity with behaviour, validation or state, dataclass.
code
python · 15 linesfrom typing import NamedTuple
class Slot(NamedTuple):
campaign: str
cpm_cents: int = 0
floor_cents: int = 0
def outbids(self, other: "Slot") -> bool:
return self.cpm_cents > other.cpm_cents
s = Slot("spring-sale", 420)
print(s) # Slot(campaign='spring-sale', cpm_cents=420, floor_cents=0)
print(Slot._field_defaults) # {'cpm_cents': 0, 'floor_cents': 0}
print(len(s), list(s), s.outbids(Slot("retarget", 90)))
print(isinstance(s, tuple)) # Truego deeper
Recall the one-line split: typing.NamedTuple is immutable and still a tuple; a dataclass is a normal mutable class unless you freeze it. Both give you a generated initialiser and repr.
Explain the concrete mechanics you would cite in review — hashability, unpacking, rightmost-defaults, the ban on extra base classes, and class-scoped equality on dataclasses versus element-wise equality on tuples.
Show judgement about where each belongs in a codebase: value objects and cache keys on one side, entities that grow fields and need validation on the other, and what converting between them costs at a boundary.
Own the standard: which record shape a team defaults to, how records evolve without breaking consumers, and when the extra safety of class-scoped equality is worth losing tuple ergonomics across a whole service.
### What the class form gives you `typing.NamedTuple` is not a different data structure from `collections.namedtuple` — it is a nicer way to declare the same thing: ```python from typing import NamedTuple class Slot(NamedTuple): """One ad slot's bid state.""" campaign: str cpm_cents: int = 0 floor_cents: int = 0 def outbids(self, other: "Slot") -> bool: return self.cpm_cents > other.cpm_cents ``` You get annotations a type checker can use, defaults written where they read naturally, methods and a docstring in the class body — and an object that is still a tuple: `len(slot)` is 3, `list(slot)` gives the values, `a, b, c = slot` unpacks, `hash(slot)` works, and `Slot._field_defaults` reports `{'cpm_cents': 0, 'floor_cents': 0}`. The functional factory can do defaults too, via `namedtuple("D", ["a", "b", "c"], defaults=(1, 2))`, which fills the **rightmost** fields — but it cannot carry annotations or methods. ### The limits of the class form Defaults must be rightmost. `class Bad(NamedTuple): x: int = 1; y: int` raises `TypeError: Non-default namedtuple field y cannot follow default field x`, the same rule that governs function parameters. Inheritance is closed. A `NamedTuple` class cannot mix in another base: `class S(Base, NamedTuple)` raises `TypeError: can only inherit from a NamedTuple type and Generic`. If your record must participate in a class hierarchy, this is the fact that decides the question for you. All fields are public, there is no per-field metadata, and validation has no hook designed for it — you would have to override construction, which is awkward enough that it is a signal you wanted a different tool. ### What a dataclass gives instead A dataclass generates the same boilerplate — initialiser, `repr`, equality — onto an ordinary class. The differences that actually drive the decision: * **Mutability is the default.** Fields can be assigned. `frozen=True` makes them read-only and makes instances hashable, which is the configuration closest to a NamedTuple. * **No sequence behaviour.** An instance has no `len()`, does not unpack, does not index, and is never mistaken for a tuple by generic code. That is a loss of convenience and a gain in safety. * **Class-scoped equality.** The generated comparison requires the same class, so two record types with identical values compare unequal — the inverse of tuple behaviour, and the reason a dataclass is safer where several record types share a shape. * **Mutable defaults done properly.** A field can take a factory that runs per instance, so a list-valued default is not shared. A NamedTuple default is a single object evaluated once at class creation, with the same sharing hazard as a mutable function default. * **Room to grow.** Inheritance, a post-initialisation hook for validation and derived fields, per-field options for excluding a field from comparison or `repr`, and a `slots=True` option that recovers most of the memory advantage. ### Choosing Pick `typing.NamedTuple` when the record is a **value**: a coordinate, a price, a parsed row, a cache key. You want it immutable, hashable, cheap, and comfortable in sequence-shaped code — a return value of several related numbers is the classic case, since the caller can either unpack it or read fields by name. Pick a dataclass when the record is an **entity or a work item**: something that gets built up in stages, validated, subclassed, or that must not be interchangeable with another record of the same shape. Anything with more than a handful of fields also argues for a dataclass, because positional construction stops being readable and tuple ordering semantics stop being meaningful. A third option worth naming: a plain `dict` when the keys are genuinely dynamic. Both record types assume a fixed, known field set; if you find yourself checking whether a field exists, neither is the right shape. ### The memory question, honestly A NamedTuple instance stores only its values, so it is markedly smaller than a dict with the same keys — on CPython 3.14, `sys.getsizeof` reports 72 bytes against 184 for three fields. A dataclass with `slots=True` lands in the same neighbourhood as the NamedTuple, so memory alone rarely decides between the two; it decides between either of them and a dict, at scale. Choose on semantics — immutability, sequence-ness, equality scope — and treat the footprint as a tiebreaker.
- Can a typing.NamedTuple class inherit from another class of your own?No. Mixing another base in raises `TypeError: can only inherit from a NamedTuple type and Generic`. You can define methods in the class body and you can make the class generic, but a mixin or an abstract base is out of reach. When a record has to sit inside a class hierarchy, that constraint alone settles the choice in favour of a dataclass.
- What happens if you give a NamedTuple field a mutable default such as an empty list?The default is evaluated once when the class is created and shared by every instance that omits it — the same trap as a mutable default argument in a function. A dataclass avoids it by letting the field take a factory that runs per instance. With a NamedTuple, use an immutable default such as an empty tuple, or a `None` sentinel.
- Does a frozen dataclass with slots make the memory argument moot?Largely, yes. `slots=True` removes the per-instance dictionary, putting a frozen dataclass in the same footprint neighbourhood as a NamedTuple. The remaining differences are semantic — sequence behaviour, class-scoped equality, inheritance and validation hooks — so decide on those and treat footprint as a tiebreaker against a plain dict rather than between the two record types.
A NamedTuple is a printed receipt — fixed, comparable, easy to file; a dataclass is a form you can still write on, initial and hand to the next desk.
saying these in an interview costs you the question
- Says the two are interchangeable with different syntax
- Thinks typing.NamedTuple instances are mutable
- Expects a frozen dataclass to unpack like a tuple
- Puts a defaulted field before a non-defaulted one
- Assumes a NamedTuple can mix in another base class
- Uses a mutable default value on a NamedTuple field