namedtuple and friends¶
This notebook covers the rest of the module: namedtuple, which gives a plain tuple's fields names, and the two smaller types OrderedDict and ChainMap. namedtuple is the one you'll reach for most — it's the lightest way to make a small, readable record.
namedtuple: a tuple with named fields¶
(51.5, -0.12) is a fine pair of coordinates until you forget which is latitude. A namedtuple keeps the tuple's lightness and immutability but lets you access fields by name, so the code reads for itself.
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y']) # or 'x y' as one string
p = Point(3, 4)
print(p) # Point(x=3, y=4) — a helpful repr, for free
print(p.x, p.y) # 3 4 — access by name
print(p[0]) # 3 — still indexable like a tuple
px, py = p # still unpacks like a tuple
print(px, py) # 3 4
It is a tuple — it compares, hashes, and unpacks like one, so it slots into anything that expects a tuple while being far more readable.
Immutable — change by making a copy¶
Like all tuples, a namedtuple can't be modified in place. To get a changed version, use _replace, which returns a new instance with some fields swapped. (The leading underscore avoids clashing with your field names; it's a public method, not a private one.)
from collections import namedtuple
Point = namedtuple('Point', 'x y')
p = Point(3, 4)
try:
p.x = 10 # tuples are immutable
except AttributeError as exc:
print('AttributeError:', exc)
moved = p._replace(x=10) # returns a new Point
print(p, '->', moved) # Point(x=3, y=4) -> Point(x=10, y=4)
The useful underscore methods¶
namedtuple adds a few helpers, all underscore-prefixed: _asdict() for a dict, _fields for the field names, _make() to build one from an iterable, and a defaults argument for optional fields.
from collections import namedtuple
Point = namedtuple('Point', 'x y')
p = Point(3, 4)
print(p._asdict()) # {'x': 3, 'y': 4}
print(Point._fields) # ('x', 'y')
print(Point._make([5, 6])) # Point(x=5, y=6) — build from a sequence (e.g. a CSV row)
# defaults fill from the right:
Server = namedtuple('Server', 'host port', defaults=[8080])
print(Server('localhost')) # Server(host='localhost', port=8080)
Where namedtuple sits¶
It fills the gap between a bare tuple and a full class:
- vs a tuple — same speed and immutability, but fields have names and a readable
repr. - vs a dict — immutable, ordered, lighter in memory, and accessed with
.fieldrather than['field']. - vs a dataclass — a
namedtupleis immutable and is a tuple (so it unpacks and compares as one); adataclassis mutable by default and better when you want methods or many fields. The classes guide compares them directly.
Reach for namedtuple for a small, fixed, immutable record — a coordinate, an RGB colour, a parsed row.
OrderedDict: order with extra tools¶
Since Python 3.7, regular dicts remember insertion order, so you rarely need OrderedDict just for that. It earns its place through a few order-specific abilities a plain dict lacks: move_to_end, popping from either end, and order-sensitive equality.
from collections import OrderedDict
od = OrderedDict([('a', 1), ('b', 2), ('c', 3)])
od.move_to_end('a') # send 'a' to the back
print(list(od)) # ['b', 'c', 'a']
od.move_to_end('a', last=False) # ...or to the front
print(list(od)) # ['a', 'b', 'c']
print(od.popitem(last=False)) # ('a', 1) — pop from the FRONT (a plain dict can't)
One more difference: OrderedDict equality is order-sensitive, while plain dicts compare equal regardless of order. This matters when order is part of the meaning (an LRU cache, an ordered config).
from collections import OrderedDict
print({'a': 1, 'b': 2} == {'b': 2, 'a': 1}) # True — order ignored
print(OrderedDict(a=1, b=2) == OrderedDict([('b', 2), ('a', 1)])) # False — order matters
ChainMap: search several dicts as one¶
ChainMap groups several dicts into a single view and searches them in order — first match wins. The classic use is layered configuration: command-line overrides on top, then environment, then built-in defaults, all looked up through one mapping without copying anything.
from collections import ChainMap
defaults = {'colour': 'black', 'size': 'medium', 'border': True}
user = {'colour': 'blue'}
settings = ChainMap(user, defaults) # search 'user' first, then 'defaults'
print(settings['colour']) # 'blue' — from user (it wins)
print(settings['size']) # 'medium' — falls through to defaults
print(dict(settings)) # merged view: {'colour': 'blue', ...}
Writes go to the first mapping only, so you can layer changes without touching the defaults — and new_child() adds a fresh top layer (handy for nested scopes).
from collections import ChainMap
defaults = {'colour': 'black', 'size': 'medium'}
settings = ChainMap({}, defaults)
settings['colour'] = 'red' # written to the first (empty) map
print(settings['colour']) # 'red'
print(defaults) # {'colour': 'black', 'size': 'medium'} — untouched
print(settings.maps) # [{'colour': 'red'}, {'colour': 'black', 'size': 'medium'}]
Recap¶
namedtupleis a tuple with named fields: readable, immutable, lightweight; change via_replace, inspect via_asdict/_fields, build via_make.- It sits between a tuple and a class — use it for small fixed records.
OrderedDictis rarely needed for ordering now, but addsmove_to_end, front-popping, and order-sensitive equality.ChainMapsearches several dicts as one (first wins) — ideal for layered configuration; writes hit only the first map.
That's the module. The Recipes put these to work, and the Concepts cover why they exist and how to choose.