skip to content

namedtuple and typing.NamedTuple

Tuples with named fields — records that still unpack, compare and hash like tuples. Interviewers ask how they stack up against dataclasses and dicts, really a question about immutability and intent.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

4

What does collections.namedtuple give you that a plain tuple does not?

level: juniorimportance: must knowfreq 62%

answer

  1. Records that still behave like sequences
  2. Names attached to tuple positions
  3. The call returns a class, not an instance
  4. Subclasses tuple; fields are read-only
  5. _fields, _asdict, _replace, _make

basics

~10 s

collections.namedtuple is a class factory: it returns a new class that subclasses tuple and gives every position a name. You can read bid.cpm_cents as well as bid[1], and the repr prints field names.

solid answer

~40 s

`collections.namedtuple("Bid", ["campaign", "cpm_cents"])` does not create a container — it generates and returns a **class** that subclasses `tuple`, with one read-only property per field. Because instances really are tuples, everything tuple-shaped keeps working: indexing, slicing, iteration, unpacking, `len()`, element-wise `==`, `hash()`, use as a dict key or set member, and passing to any API that expects a sequence. What you add on top is name access (`bid.cpm_cents` instead of the unexplained `bid[1]`), a `repr` that shows the field names, and a small helper API — `_fields`, `_asdict()`, `_replace()` and `_make()`. What you do not get is mutability: fields are read-only, instances carry no `__dict__`, so you cannot assign to a field or bolt on a new attribute.

code

python · 12 lines
python
from collections import namedtuple

Bid = namedtuple("Bid", ["campaign", "cpm_cents"])
b = Bid("spring-sale", 420)

print(b.cpm_cents, b[1])          # 420 420
print(b == ("spring-sale", 420))  # True

campaign, cpm = b                 # tuple unpacking still works
print(campaign, cpm)              # spring-sale 420
print(b)                          # Bid(campaign='spring-sale', cpm_cents=420)
print(b._fields, b._asdict())

go deeper

for a junior

Be ready to state that collections.namedtuple returns a class subclassing tuple, and to show both b.field and b[0] working on the same object. Knowing that instances are immutable is the other half of the answer.

for a middle

Explain the mechanics: read-only properties over tuple slots, empty __slots__ so there is no per-instance __dict__, and the _fields/_asdict/_replace/_make helper API plus why it is underscore-prefixed.

for a senior

An interviewer expects you to weigh it against a dict and a dataclass for a real record type, and to note the consequences of tuple identity — hashability, unpacking, and code elsewhere treating your record as a sequence.

for a principal

Own the convention question: when a codebase should standardise on tuple-shaped records versus class-shaped ones, and what that choice costs later when a record has to gain a field, gain validation, or cross a serialization boundary.

### The factory, not the container The single most common misreading is that `namedtuple(...)` returns a record. It returns a **class**. At call time `collections.namedtuple` builds the source of a new class, executes it, and hands back a class object whose base is `tuple`. You then call that class to make instances: ```python from collections import namedtuple Bid = namedtuple("Bid", ["campaign", "cpm_cents"]) # a class b = Bid("spring-sale", 420) # an instance ``` The first argument is the name the generated class carries in `__name__` and in its `repr`. Nothing enforces that it matches the variable you bind it to; `X = namedtuple("Bid", ...)` works and then prints `Bid(...)`, which is exactly the kind of confusion that shows up in logs. Field names may be a list of strings or one whitespace- or comma-separated string: `namedtuple("Bid", "campaign cpm_cents")`. ### What you gain **Names for positions.** `b.cpm_cents` says what `b[1]` means. On a record read in three files and written in one, that is the whole value proposition. **A readable repr.** `Bid(campaign='spring-sale', cpm_cents=420)` instead of `('spring-sale', 420)`. Logs and tracebacks become self-describing. **A small helper API.** `Bid._fields` is the tuple of field names, `b._asdict()` converts to a dict, `b._replace(cpm_cents=500)` returns a modified copy, and `Bid._make(row)` builds an instance from an existing iterable — the natural way to turn a CSV row or a database row into a record. ### What you keep, because it is still a tuple This is the part that separates a namedtuple from a small class. An instance passes `isinstance(b, tuple)`, so it indexes, slices, iterates, unpacks (`campaign, cpm = b`), supports `in` and `len()`, compares element-wise with `==`, orders lexicographically field by field, hashes when its fields hash, and can therefore live in a `set` or serve as a dict key. It splats into a call with `*b`, and it pickles by value. Any code path written against sequences — including third-party code you do not control — accepts it unchanged. ### What you give up **Mutability.** Each field is a read-only property over a tuple slot, so `b.cpm_cents = 500` raises `AttributeError`. Use `_replace()` to get a new instance. **Ad-hoc attributes.** The generated class defines an empty `__slots__`, so instances carry no `__dict__` and `b.extra = 1` raises `AttributeError` as well. That is a feature for record types and a wall if you wanted a scratch object. **Type checking.** The functional form carries no annotations; the fields accept anything. `typing.NamedTuple` is the class form that adds annotations (and lets you define methods and docstrings in the class body). ### Cost Per-instance memory is the tuple's own array with no dict overhead. On CPython 3.14, `sys.getsizeof` reports 72 bytes for a three-field namedtuple instance against 184 for the equivalent three-key dict — relevant when you hold millions of records, irrelevant for a handful. Attribute access goes through a per-field descriptor: slightly slower than reading a plain instance attribute, in the same neighbourhood as indexing. ### Why the helpers start with an underscore `_fields`, `_field_defaults`, `_asdict`, `_replace` and `_make` are not private. The leading underscore exists because **every name without one is potentially a field name** — a record with a field called `fields` or `replace` must still work. Reserving the underscore prefix keeps the API from colliding with your data. ### Version notes `_asdict()` returned an `OrderedDict` up to Python 3.7 and returns a plain `dict` from 3.8 onward, since dicts have kept insertion order since 3.7. The `defaults=` parameter arrived in 3.7. From 3.13, `copy.replace()` works on namedtuple instances and does the same job as `_replace()`. ### Where it sits among the alternatives Reach for a dict when the keys are dynamic or the shape varies. Reach for `typing.NamedTuple` when you want the same tuple semantics with annotations, defaults and methods in a class body. Reach for a dataclass when the record must be mutable, needs validation, or must **not** behave like a sequence.

  • How do you build a namedtuple instance from a sequence you already have, such as a database row?
    Use the generated class's `_make()` classmethod: `Bid._make(row)` consumes any iterable positionally and returns an instance. Calling `Bid(*row)` works too, but `_make` avoids the splat and reads better in a comprehension over many rows. Both raise `TypeError` if the row has the wrong length, which is usually the behaviour you want on malformed input.
  • Why do the helper methods start with a single underscore if they are part of the public API?
    Because every name without an underscore is a candidate field name. A record could legitimately have a field called `fields` or `replace`, and that must not shadow the API. Reserving the underscore prefix guarantees the two namespaces never collide. They are documented and intended for use — the underscore signals namespace hygiene here, not privacy.
  • Can you add methods to a namedtuple class?
    Not in the functional form, but you can subclass the generated class and add methods there, or use the `typing.NamedTuple` class form and write the methods directly in the class body alongside annotated fields. The class form is the idiomatic choice when a record needs behaviour as well as data.

A plain tuple is a row of unlabelled boxes you have to count into; a namedtuple is the same row with a label under each box — nothing has moved, you have just stopped counting.

saying these in an interview costs you the question

  • Says namedtuple returns a record rather than a class
  • Claims namedtuple instances are mutable
  • Thinks index access stops working once fields have names
  • Believes you can attach new attributes to an instance
  • Calls it a dict subclass rather than a tuple subclass
  • Assumes the helpers are private because of the underscore

context

open as a page

How do you change a field value on a collections.namedtuple instance?

level: middleimportance: must knowfreq 55%

basics

~20 s

You do not change it in place — namedtuple fields are read-only. Call the instance's _replace() method with keyword arguments; it returns a new instance with those fields swapped and the rest copied, leaving the original untouched.

open as a page

When would you choose typing.NamedTuple over a dataclass for a record?

level: middleimportance: should knowfreq 52%

basics

~20 s

Choose 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.

open as a page

When does a namedtuple's tuple compatibility become a production hazard?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

A namedtuple instance really is a tuple, so unrelated code treats it as a sequence: two different record types with equal values compare equal, JSON encodes it as an array, and percent-formatting unpacks it as arguments.

open as a page