How do you change a field value on a collections.namedtuple instance?
answer
- In-place assignment is not an option
- You get a new object, not an edit
- Keyword arguments name the fields to swap
- Result must be rebound or it is lost
- _replace, and copy.replace since 3.13
basics
~20 sYou 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.
solid answer
~40 sAssignment fails: each field is a read-only property over a tuple slot, so `bid.cpm_cents = 500` raises `AttributeError`, and the class's empty `__slots__` means you cannot sidestep it with a new attribute either. The supported move is `new = bid._replace(cpm_cents=500)`, which builds a fresh instance from the old field values with the named ones overridden and returns it — the original is unchanged, so you must rebind or store the result. Passing a name that is not a field raises `TypeError` rather than silently adding one, which is a genuine advantage over dict mutation. From Python 3.13 `copy.replace(bid, cpm_cents=500)` does the same thing through a generic protocol. `_replace` is a shallow operation: a mutable object stored inside a field is shared with the copy.
code
python · 16 linesimport copy
from collections import namedtuple
Bid = namedtuple("Bid", "campaign cpm_cents")
b = Bid("spring-sale", 420)
try:
b.cpm_cents = 500
except AttributeError as exc:
print("AttributeError:", exc)
raised = b._replace(cpm_cents=500)
print(raised) # Bid(campaign='spring-sale', cpm_cents=500)
print(b) # unchanged: cpm_cents=420
print(copy.replace(b, cpm_cents=600)) # same effect, Python 3.13+
print(Bid._make(["retarget", 90])) # build from a sequencego deeper
Remember that a namedtuple cannot be edited in place and that _replace() hands back a new object. Rebinding the result is the part most people forget the first time.
Explain why assignment fails — read-only descriptors over tuple slots plus an empty __slots__ — and that _replace rejects unknown field names with TypeError where a dict would invent a key.
Show the production angle: immutable records make partial-failure rollback and cross-thread sharing trivial, and be precise that _replace is shallow so mutable field values are still shared.
Own the allocation and convention tradeoff — when a copy-on-write record type is worth the object churn in a hot path, and when a mutable record with explicit locking or an undo log is the better fit.
### Why assignment fails `collections.namedtuple` generates a class whose base is `tuple`, and a tuple's storage is fixed at construction. Each field is exposed as a read-only descriptor over one slot of that storage, so there is no setter to run: ```python from collections import namedtuple Bid = namedtuple("Bid", "campaign cpm_cents") b = Bid("spring-sale", 420) b.cpm_cents = 500 # AttributeError: can't set attribute ``` The generated class also declares an empty `__slots__`, so instances carry no `__dict__`. That closes the usual escape hatch: `b.extra = 1` raises `AttributeError` too, with a message that names the missing `__dict__` explicitly. A namedtuple is immutable in the same sense a tuple is — the record's own bindings cannot be re-pointed. This is deliberate rather than an oversight. Hashability depends on it: because the field values cannot change, `hash(b)` is stable for the object's whole life, which is what makes a namedtuple safe as a dict key or set member. A record type that allowed field mutation could not offer that guarantee without becoming a source of silently corrupted lookups. ### The replacement idiom ```python raised = b._replace(cpm_cents=500) # Bid(campaign='spring-sale', cpm_cents=500) # b is untouched: Bid(campaign='spring-sale', cpm_cents=420) ``` `_replace()` takes **keyword arguments only**, one per field you want to change, and returns a new instance of the same class with every other field copied across. Three details matter in review: 1. **It returns; it does not mutate.** Calling `b._replace(cpm_cents=500)` and discarding the result is a no-op bug, and a common one — it reads like a mutator. The value has to be rebound or stored. 2. **Unknown field names raise.** `b._replace(cpc=500)` raises `TypeError: Got unexpected field names: ['cpc']`. Compare with a dict, where `d["cpc"] = 500` cheerfully invents a key and the typo survives to production. 3. **It is shallow.** If a field holds a list, the new instance shares that same list object with the old one. Mutating it mutates both. For records that must be genuinely independent, either keep the fields immutable (a nested namedtuple, a `frozenset`, a `tuple`) or take a `copy.deepcopy` explicitly. ### Chaining and the alternatives Several fields can be changed in one call: `b._replace(campaign="retarget", cpm_cents=90)`. Chained calls each allocate an instance, so a hot loop that walks a record through five edits allocates five objects; build the final values first and make one call when that matters. Other ways to reach the same place: * `Bid(*old_values)` — positional reconstruction. Works, but every reordering or added field silently breaks it, which is exactly the failure `_replace` exists to prevent. * `Bid(**{**b._asdict(), "cpm_cents": 500})` — verbose, allocates a dict, and loses the unknown-name check. `_replace` does this internally with less ceremony. * `copy.replace(b, cpm_cents=500)` — available since Python 3.13. It is the generic spelling that also works on dataclass instances and several stdlib types, so a helper that must handle more than one record kind can use it uniformly. ### Where this bites in practice Consider an ad-auction bidder that assembles a bid record from a base template, then adjusts it as pacing and floor-price rules apply. If those rules mutated a shared record, a partial-failure rollback would have nothing to roll back to: the pre-adjustment state is gone. With namedtuple records, each rule returns a new instance and the original is still in hand, so unwinding to the last good record is a rebinding rather than an undo log. That is the practical argument for immutable records in any pipeline where you may have to abandon half-applied work. The same property makes concurrency simpler. An immutable record handed to another thread cannot be changed underneath it, so no lock is needed to read it — the coordination problem shrinks to publishing the new reference. ### The related helpers `_asdict()` gives a plain `dict` (an `OrderedDict` before Python 3.8) when you need to hand the record to something dict-shaped. `_fields` is the tuple of field names, useful for writing a CSV header or building a generic diff between two records of the same type. `_make(iterable)` is the inverse of iteration — it constructs an instance from a positional sequence. Together with `_replace` these four are the whole mutation-and-conversion story for tuple-shaped records.
- Is _replace a deep copy?No. It builds a new instance from the existing field values, so any mutable object stored in a field — a list, a dict, another mutable record — is shared between the old and new instances. Mutating it is visible through both. Keep nested values immutable if you want the record to be effectively immutable, or take an explicit `copy.deepcopy` when you truly need independent state.
- If the fields are read-only, why can a namedtuple still hold something that changes?Immutability applies to the bindings, not to what they point at. A field holding a list cannot be re-pointed at a different list, but the list itself can be appended to. That also breaks hashability at runtime: `hash()` on a record containing a list raises `TypeError`, so such a record cannot be a dict key even though the class in general can.
- When would you use copy.replace instead of _replace?When the code must handle more than one record kind. Since Python 3.13, `copy.replace()` works on namedtuple instances, dataclass instances and several stdlib types through one protocol, so a generic helper can adjust a field without caring which record flavour it received. For a single known namedtuple type, `_replace()` is shorter and equally correct.
It is a photocopy with one line corrected, not an erasure: the original page is still on the desk, and if you walk away without picking up the copy, nothing has changed.
saying these in an interview costs you the question
- Says _replace mutates the instance in place
- Calls _replace and discards the returned value
- Thinks setattr can bypass the read-only field
- Assumes _replace deep-copies nested mutable values
- Expects a misspelled field name to be added silently
- Claims namedtuples cannot be modified in any sense at all