skip to content

In a typing.NamedTuple class, how do you give fields defaults and add methods?

level: juniorimportance: must knowfreq 65%

answer

  1. Two kinds of name in the body
  2. Only annotated names become fields
  3. It compiles down to a signature
  4. Defaults live on __new__.__defaults__
  5. Defaulted fields must be last

basics

~20 s

Write the record as a class: an annotated name with an assigned value becomes that field's default, and ordinary def statements become real methods on the generated tuple subclass. Defaulted fields must come last, exactly as with function parameters.

solid answer

~50 s

`typing.NamedTuple` supports a class body, and that body is split by kind. Annotated names become the fields, in declaration order; an annotated name that is also assigned a value supplies a default for the generated `__new__`; and anything else — `def`s, `property` objects, unannotated constants — is copied straight onto the generated `tuple` subclass. So defaults and behaviour live in one readable block. The ordering rule is not tuple-specific: the constructor is a normal signature like `__new__(_cls, url, attempts=0)`, and a required parameter cannot follow a defaulted one, so declaring a plain field after a defaulted one raises `TypeError` at class-creation time. Defaults are evaluated once, at class creation, and land in `__new__.__defaults__` and `_field_defaults` — which means a mutable default such as `[]` is shared by every instance. `collections.namedtuple` takes a `defaults=` sequence but has no body, so methods there require subclassing the generated class.

code

python · 22 lines
python
from typing import NamedTuple


class Delivery(NamedTuple):
    url: str
    attempts: int = 0
    delivered: bool = False

    @property
    def exhausted(self) -> bool:
        return self.attempts >= 3

    def retried(self) -> "Delivery":
        return self._replace(attempts=self.attempts + 1)


d = Delivery("https://example.test/hook")
print(d)
print(Delivery._fields, Delivery._field_defaults)
print(Delivery.__new__.__defaults__)
print(d.retried().retried().retried().exhausted)
print(isinstance(d, tuple), d[0], d.url)

go deeper

for a junior

Be ready to write the class form from memory: annotated names are the fields, an assigned value is that field's default, defaulted fields go last, and a def in the body is just a method. Knowing that instances are still ordinary tuples is expected.

for a middle

Explain the mechanics: the body is split into fields, defaults and everything else; the defaults become the generated new's parameter defaults, evaluated once at class creation and visible in _field_defaults; and the ordering error is raised at import time, not at construction.

for a senior

Show the production judgement: a shared mutable default silently coupling instances, field order being a public contract for both positional construction and unpacking, and why a namedtuple subclass that adds behaviour needs slots = () to keep the memory win.

for a principal

Own the tradeoff of standardising on tuple-backed records at all: positional fields make ordering a breaking change across services, there is no keyword-only escape, and defaults invite silent field drift — decide where that beats a heavier record type and write the convention down.

### The class form is a factory, not an ordinary base class `typing.NamedTuple` looks like inheritance, but you are not inheriting behaviour from it. When the class body finishes executing, the machinery behind `NamedTuple` reads that body's namespace and splits it into three piles: 1. **Annotated names** — `url: str`, `attempts: int` — become the record's *fields*, in the exact order they were written. That order is both the tuple's index order and the parameter order of the generated `__new__`. 2. **Annotated names that were also assigned a value** — `attempts: int = 0` — additionally contribute a *default* for that parameter. 3. **Everything else** — `def` statements, `property` objects, `classmethod` and `staticmethod` objects, docstrings, unannotated assignments — is copied onto the generated class untouched. What comes out is a genuine subclass of `tuple` carrying `_fields`, `_field_defaults`, `_replace`, `_asdict` and `_make`, a `__new__` whose signature you can print with `inspect.signature`, and whatever methods you wrote. The methods are real methods: they are looked up on the class like any other, they are not stored per instance, and they do not occupy tuple slots. ### Why the defaulted fields must come last There is nothing tuple-specific about this rule; it is Python's own parameter grammar. The generated constructor for a record with `url`, `attempts = 0` and `delivered = False` is effectively `def __new__(_cls, url, attempts=0, delivered=False)`, and a required parameter cannot follow a defaulted one in any Python signature. So the class-creation machinery checks the declaration order itself and raises `TypeError: Non-default namedtuple field b cannot follow default field a` at *import* time, not when you first construct an instance. A dataclass hits the same wall and offers a keyword-only escape hatch; a NamedTuple has none, because its fields are also positional tuple slots and the positions are the record's identity. The practical consequence for design: field order is part of your public contract twice over — once for positional construction, once for indexing and unpacking — so a field that gains a default later must already be at the end, and reordering fields to make room is a breaking change for every caller that unpacks or indexes. ### Where the defaults live, and when they are evaluated `Delivery.__new__.__defaults__` holds the default values as a tuple, and `Delivery._field_defaults` exposes them as a `{name: value}` mapping. Both are built **once**, at class-creation time, from expressions evaluated in the class body — exactly like ordinary function parameter defaults. That produces the same classic trap: a mutable default is shared. Writing `tags: list = []` gives *every* default-constructed instance the same list object, and mutating it through one instance is visible through all of them. The `@dataclass` decorator rejects a mutable default outright; `NamedTuple` does not, and the tuple's own immutability does not save you, because a tuple is only shallowly immutable — the slot cannot be rebound, but the list inside it can be mutated. If you want a per-instance empty collection, default to `None` (or to an immutable empty tuple) and build the collection in a small factory `classmethod`. ### Methods with `collections.namedtuple`, and why the class form is easier `collections.namedtuple` is a string-driven factory: you pass a class name, the field names, and — since Python 3.7 — a `defaults` sequence that is applied to the *rightmost* fields. It returns a finished class, and there is no class body in which to put a `def`. To attach behaviour you subclass the class it returned and set `__slots__ = ()` on the subclass so instances do not acquire a per-instance `__dict__`, which would quietly undo the memory advantage the record type exists for. The class form does all of that for you in one block, which is the main day-to-day reason to reach for `typing.NamedTuple`: annotations, defaults and behaviour in one readable place, with the annotations doubling as documentation and as input to a static type checker. ### The escape hatch that is also a trap An assignment **without** an annotation is not a field. `SCALE = 10` in the body stays an ordinary class attribute: it does not appear in `_fields`, it does not become a `__new__` parameter, and it costs nothing per instance. That is the intended way to hang a constant off the record. It is also the failure mode to watch for in review — forget the annotation on a name you meant as a field and it silently vanishes from the record, with no error until something downstream unpacks the wrong number of values. ### Version notes The class form with per-field defaults has behaved this way since Python 3.6.1; `collections.namedtuple`'s `defaults=` argument arrived in 3.7. Nothing here changed through 3.14. In particular, 3.14's deferred evaluation of annotations (PEP 649) does not disturb it: the field list is derived from the annotated *names* in the class body and their declaration order, not from evaluating the annotation expressions, so a record whose annotations reference a type defined later in the module still builds its fields, its order and its defaults correctly.

  • Where do a typing.NamedTuple's defaults actually live, and when are they evaluated?
    They are evaluated once, while the class is being created, and stored on the generated constructor: `Delivery.__new__.__defaults__` is the tuple of values, and `Delivery._field_defaults` is the same thing as a name-to-value mapping. Because evaluation happens once, a mutable default like `[]` is one shared object across every default-constructed instance — default to `None` and build the collection in a factory classmethod instead.
  • How would you attach the same helper method to a class made by collections.namedtuple?
    Subclass it. Call `namedtuple(...)` to get the base class, then define your own class inheriting from it with the method on it, and set `__slots__ = ()` in that subclass so instances do not gain a per-instance `__dict__`. The typing.NamedTuple class form collapses those two steps into one block, which is why it is the usual choice when a record needs behaviour.
  • Does an assignment without an annotation in a NamedTuple class body become a field?
    No. Only annotated names become fields. `SCALE = 10` with no annotation stays an ordinary class attribute: it is absent from `_fields`, it is not a parameter of the generated `__new__`, and it adds nothing to each instance. That makes it the right way to hang a constant on the record — and a real review hazard, since forgetting an annotation on an intended field silently drops it from the record.

The class body is a sorting hatch, not a blueprint: annotated names go into the record's slots, an annotated name with a value goes in carrying a default tag, and everything else is handed through unchanged to the finished class.

saying these in an interview costs you the question

  • Thinks a def in the class body becomes another tuple field
  • Declares a required field after a defaulted one and expects it to work
  • Believes the defaults are re-evaluated on every construction
  • Uses [] as a field default assuming each instance gets its own list
  • Claims collections.namedtuple cannot have default values at all
  • Thinks adding a method makes instances mutable or no longer tuples

context