PYTHON / LISTS AND TUPLES
Named tuples
Define namedtuple and typing.NamedTuple record types, read fields by name or index, and build modified copies with _replace.
What you will learn
- Create record types with collections.namedtuple and typing.NamedTuple
- Read the same field by attribute name or by numeric index
- Produce a changed copy with _replace instead of assigning to a field
- Convert to a dict with _asdict and list field names with _fields
Understanding Named tuples
A plain tuple stores position, not meaning: reading row[2] forces you to remember what slot 2 held. A named tuple is a tuple subclass generated for you, where each position also has an attribute name. Because it subclasses tuple, every tuple operation you already know still works on it, and nothing about immutability changes.
collections.namedtuple is a class factory: you call it with a type name and a sequence of field names, and it returns a new class. The field names can be given as a list of strings or as one space- or comma-separated string, which is why namedtuple("Point", "x y") and namedtuple("Point", ["x", "y"]) produce the same class. That generated class defines __slots__ and no instance dictionary, so an instance costs about the same memory as the tuple it replaces, and you cannot attach extra attributes to it.
Since instances are immutable, the API gives you copy-with-changes instead of mutation: p._replace(y=10) builds a fresh instance and leaves p untouched. The leading underscore on _replace, _asdict, and _fields is not a privacy marker; it exists so those method names cannot collide with your own field names, which is also why fields themselves may not start with an underscore. Equality and ordering are still inherited from tuple, so comparisons look only at the values in order and completely ignore the field names and the class.
from collections import namedtuple
Point = namedtuple("Point", "x y")
p = Point(3, 4)
print(p)
print(p.x, p[1])
print(p._fields)
moved = p._replace(y=10)
print(moved)
print(p == Point(3, 4))
x, y = p
print(x + y)A named tuple is a tuple subclass that adds attribute names to positions without adding mutability or changing tuple comparison behaviour.
Worked examples
Records in a list
Named tuples let sort and max keys read as field names instead of index numbers.
from collections import namedtuple
Reading = namedtuple("Reading", ["sensor", "celsius"])
data = [Reading("kitchen", 21.5), Reading("attic", 30.2), Reading("cellar", 12.0)]
hottest = max(data, key=lambda r: r.celsius)
print(hottest.sensor)
for r in sorted(data, key=lambda r: r.sensor):
print(r.sensor, r.celsius)
print(data[0]._asdict())Example explained
Line 1key=lambda r: r.celsius states the sort field by name, so a later field reordering cannot silently break it.
Line 2sorted returns a new list of the same Reading objects; the named tuples themselves are never modified.
Line 3_asdict() returns a regular dict mapping field names to values, which is handy for JSON output.
Still a tuple underneath
Shows that comparison ignores field names and class, and that concatenation degrades to a plain tuple.
from collections import namedtuple
Pair = namedtuple("Pair", "a b")
Duo = namedtuple("Duo", "first second")
print(Pair(1, 2) == Duo(1, 2))
print(Pair(1, 2) == (1, 2))
print(type(Pair(1, 2) + (3,)).__name__)
print(Pair(2, 1) > Pair(1, 99))Example explained
Line 1Pair and Duo are unrelated classes, but tuple.__eq__ compares values position by position, so they are equal.
Line 2Comparing against the bare tuple (1, 2) also succeeds for the same reason.
Line 3Concatenation calls tuple.__add__, which builds a plain tuple: the result has three values and no field names.
Line 4Ordering is lexicographic on values, so 2 > 1 in the first position decides the comparison immediately.
typing.NamedTuple with a default and a method
The class syntax adds annotations, per-field defaults, and normal methods to a named tuple.
from typing import NamedTuple
class Rect(NamedTuple):
width: float
height: float = 1.0
def area(self) -> float:
return self.width * self.height
r = Rect(3.0)
print(r)
print(r.area())
print(len(r), tuple(r))Example explained
Line 1height: float = 1.0 supplies a default, so Rect(3.0) fills the second field automatically.
Line 2Fields with defaults must come after fields without them, exactly as in a function signature.
Line 3area is an ordinary method; it does not become a field because it has no annotation at class level.
Line 4len(r) is 2 and tuple(r) yields the values in declaration order, confirming it is a real tuple.
Important notes
Field names must be valid identifiers, cannot be Python keywords, cannot start with an underscore, and cannot repeat; pass rename=True to namedtuple to have offending names replaced by positional names like _1.
Named tuples have no instance __dict__, so you cannot add an attribute after construction; if you need mutable fields, reach for a dataclass instead.
Common mistakes
Assigning to a field, as in p.x = 5, which raises AttributeError because the class defines only read-only property descriptors over tuple slots.
Calling p._replace(y=10) and then reading p, which still holds the old value; _replace returns a new instance and discards it if you do not assign the result.
Reading the underscore in _replace or _fields as private and copying the values out by index instead; those are the documented public API, underscored only to keep the field namespace free.
Try it yourself
Change, predict, then run
Define Employee = namedtuple("Employee", "name department salary"), build a list of three employees, print the name of the highest paid one using a key that refers to the field by name, then print a copy of the first employee with the salary raised by 10 percent while showing that the original is unchanged.
Open the Python workspaceCheck your understanding
Given Pair = namedtuple("Pair", "a b") and Duo = namedtuple("Duo", "first second"), why does Pair(1, 2) == Duo(1, 2) evaluate to True?
- Equality is inherited from tuple and compares values by position, ignoring the class and the field names
- namedtuple caches generated classes, so Pair and Duo are actually the same class
- The two classes have the same number of fields, so Python matches fields pairwise by name
- Named tuples define __eq__ to compare _asdict() results, which happen to match here
Show answer
A named tuple adds attribute access but does not override tuple's __eq__, so only the ordered values are compared and both types and field names are irrelevant. The _asdict() option is tempting but wrong: those dicts are {'a': 1, 'b': 2} and {'first': 1, 'second': 2}, which are not equal, so if that were the rule the result would be False.