skip to content

How do you update a field on a collections.namedtuple instance?

level: middleimportance: should knowfreq 45%

answer

  1. You never assign to a field
  2. Build a modified copy instead
  3. One keyword-argument helper returns a new instance
  4. Underscored names avoid colliding with fields
  5. Copies are shallow, not deep

basics

~10 s

You do not update it in place — the record is immutable and attribute assignment raises AttributeError. Call _replace(field=value), which returns a new instance with that field changed and the rest copied across.

solid answer

~50 s

Records are immutable, so the update idiom is to build a modified copy: `_replace()` takes the changed fields as keyword arguments, applies them over the current values and returns a new instance of the same class. It is shallow — nested mutable objects are shared with the original, not copied — and a misspelled field name raises `TypeError` naming the unexpected argument, which is a useful guard the equivalent dict-based code lacks. The helpers all carry a leading underscore — `_replace()`, `_asdict()`, `_make()`, `_fields`, `_field_defaults` — because field names themselves are forbidden from starting with an underscore, so the API can never collide with your data. `_asdict()` returns a plain `dict` you can mutate or serialise, and `_make()` builds an instance from any iterable, which is the natural way to turn an incoming row into a record.

code

python · 8 lines
python
from collections import namedtuple

Delivery = namedtuple("Delivery", "url attempts", defaults=(0,))
d = Delivery("https://example.test/hook")
retried = d._replace(attempts=d.attempts + 1)
print(d, retried)
print(retried._asdict())
print(Delivery._make(["https://example.test/other", 2]))

go deeper

for a junior

Know that a record cannot be changed in place and that the update idiom returns a new instance. Be able to name the helper that produces the modified copy and read its result.

for a middle

Explain why the helpers are underscored, that the copy is shallow, and that an unknown field name raises rather than being accepted — and show _make() and _asdict() on either side of the boundary.

for a senior

Discuss the consequences in running code: copy-per-update in hot paths, shared nested mutables defeating the immutability you advertised, and when that pushes a value type to a mutable object instead.

for a principal

Frame the convention: which values in a system are immutable by contract, how updates flow as new values rather than mutations, and what that buys in shared caches and concurrent code.

**There is no in-place update.** A tuple-backed record stores its values in the tuple's own storage, and the generated field accessors are read-only descriptors; the class also declares empty `__slots__`, so there is no instance dictionary to stash a shadowing attribute in. `d.attempts = 1` raises `AttributeError: can't set attribute`. That is the whole point of the type: once built, a record is a value, safe to share between threads, safe to use as a dict key, and safe to hand to a caller without defensive copying. **The copy-with-changes idiom.** `_replace()` is the supported way to move to a new value: ```python retried = delivery._replace(attempts=delivery.attempts + 1) ``` It maps the current record to a dict of field-to-value, overlays the keyword arguments you passed, and builds a fresh instance of the same class from the result. The original is untouched. Three properties are worth stating explicitly in an interview. It is **shallow**: if a field holds a list, the new record shares that same list object with the old one, so mutating it changes what both records see — use `copy.deepcopy` first if that matters. It is **checked**: passing a name that is not a field raises `TypeError: Got unexpected field names: [...]`, so a typo fails loudly where a dict update would silently add a key. And it **returns the same class**, so a subclass of a record stays that subclass. **Why every helper is underscored.** This looks like a private API and is deliberately not one. A record's field names come from the caller — sometimes from a file header or a database cursor — so any public method name the class defined could be shadowed by a legitimate field. The design resolves the clash by reserving the underscore namespace: field names may not begin with an underscore (`namedtuple("Bad", ["_x"])` raises `ValueError: Field names cannot start with an underscore`), and every helper the class provides lives there. `_fields`, `_field_defaults`, `_make()`, `_asdict()` and `_replace()` are public API despite their spelling; a candidate who calls them private has read the convention without reading the reason. **The rest of the small API.** - `_fields` is a tuple of the field names in order. It is what you iterate to build a header row, and `record._fields` combined with `zip` gives you name/value pairs without a dict. - `_field_defaults` maps the defaulted field names to their default values; empty when none were declared. - `_make(iterable)` is a classmethod that builds an instance from any iterable of the right length. It is the fast path for turning a parsed row or a database tuple into a record, and it is what `_replace()` uses internally. - `_asdict()` returns a plain `dict` of field name to value. Use it when something downstream needs a mapping — most importantly serialisation, since encoding the record directly writes a JSON array rather than an object. **Patterns this enables.** Because updates are copies, a pipeline stage reads naturally as a chain of transformations: each stage takes a record and returns a new one, and nothing upstream can observe a half-applied change. Where a record is a cache key or lives in a set, immutability is what makes that legal at all — a mutable record whose fields changed after insertion would sit in the wrong hash bucket forever. And where the same value must be updated frequently in a hot loop, the copying is real work: allocating a new tuple per change is cheap but not free, and if you are doing it thousands of times per item, that is a signal the value should be a mutable object rather than a record. **Common mistakes.** Writing `record._replace(attempts=1)` and throwing the result away, expecting mutation — `_replace()` returns; it does not modify. Reaching for `_asdict()`, mutating the dict and rebuilding with `**` when `_replace()` says the same thing more clearly and validates the names. Calling the underscored helpers 'private' and hand-rolling replacements for them. And assuming the copy is deep: it is not, and shared nested state is the classic way an 'immutable' record turns out to have been mutable all along. **When copying stops being pleasant.** If most fields change on most updates, or updates happen in a tight loop, the copy-with-changes idiom is fighting you. That is the signal to move from a tuple-backed record to a mutable object, and it is a design decision rather than a syntax one.

  • Why do the helper names on a namedtuple class start with an underscore?
    To keep the class's own API out of the caller's namespace. Field names are supplied by the caller and could otherwise shadow a method, so the design forbids field names that begin with an underscore — the factory raises `ValueError` for one — and puts every helper there instead. `_fields`, `_asdict()` and `_replace()` are public API despite the spelling.
  • Is the copy produced by _replace shallow or deep?
    Shallow. Only the fields you name are substituted; every other field is carried over as the same object, so a list or dict inside the record is shared with the original and mutating it is visible through both. Use `copy.deepcopy` explicitly when independence matters — or, better, keep only immutable values in a record so the question does not arise.
  • What happens if you misspell a field name in a _replace call?
    It raises `TypeError` and names the unexpected fields, so the typo fails at the call rather than silently creating a nonsense record. That is a small but real advantage over the `_asdict()`-mutate-rebuild pattern, where an unknown key would be rejected only at reconstruction, and over dict-based records, where it would just be added.

It is a printed form rather than a whiteboard: you never rub out one line, you photocopy the form with the one box filled in differently.

saying these in an interview costs you the question

  • Thinks _replace mutates the record in place
  • Calls _fields and _asdict private, off-limits API
  • Assumes the copy is deep and nested state is independent
  • Says a misspelled field name is silently added
  • Suggests object.__setattr__ to force a field change

context