collections: The Stdlib's Hidden Power
1 · The lesson
readPlain dict, list, and tuple cover most jobs. But every Python developer eventually writes a dict.get(key, 0) + 1 pattern, or a dict.setdefault(key, []).append(...), or a list-pop-from-front that's secretly O(n). The collections module is the standard library's answer: small, sharp tools for patterns that show up over and over.
This lesson walks through the six containers you'll actually use — Counter, defaultdict, deque, namedtuple, ChainMap, OrderedDict — with the decision rules for when to pick each.
1. Counter — The Universal Tally
Counter is a dict subclass purpose-built for counting hashable things.
from collections import Counter words = "the quick brown fox jumps over the lazy dog the".split() c = Counter(words) print(c) # Counter({'the': 3, 'quick': 1, 'brown': 1, ...}) print(c["the"]) # 3 print(c["missing"]) # 0 — missing keys return 0, not KeyError
The signature accepts any iterable. Want character frequencies? Pass a string. Vote tallies? Pass a list of choices.
print(Counter("mississippi")) # Counter({'i': 4, 's': 4, 'p': 2, 'm': 1})
setup added so this can run · defines Counter
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def Counter(*_a, **_kw): print('-> Counter() called') return _AutoMock('Counter()')
most_common(n) gives you the top-n in one call — pass n when you only want a slice, not the whole sorted list:
print(c.most_common(2)) # [('the', 3), ('quick', 1)]
setup added so this can run · defines c
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) c = _AutoMock('c')
Counters also support arithmetic:
a = Counter("aabbc") # a:2 b:2 c:1 b = Counter("abbcc") # a:1 b:2 c:2 print(a + b) # union sum → a:3 b:4 c:3 print(a - b) # subtract, drop zero/negative → a:1 print(a & b) # min (intersection) → a:1 b:2 c:1 print(a | b) # max (union) → a:2 b:2 c:2
setup added so this can run · defines Counter
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def Counter(*_a, **_kw): print('-> Counter() called') return _AutoMock('Counter()')
Useful for anything from word-count diffs to inventory reconciliation in two lines.
2. defaultdict — Auto-Creating Values
The classic "group items by key" loop in plain dict:
people = [("eng", "Alice"), ("eng", "Bob"), ("design", "Carol"), ("eng", "Dan")] by_team = {} for team, name in people: by_team.setdefault(team, []).append(name)
defaultdict(list) makes it cleaner:
from collections import defaultdict by_team = defaultdict(list) for team, name in people: by_team[team].append(name) # no setdefault — first access creates [] print(dict(by_team)) # {'eng': ['Alice', 'Bob', 'Dan'], 'design': ['Carol']}
setup added so this can run · defines people
people = [("alpha", 1), ("beta", 2), ("gamma", 3)]
The argument is a factory — a callable that returns the default value. list for grouping, set for de-duped grouping, int for counting:
counts = defaultdict(int) for word in "to be or not to be".split(): counts[word] += 1 # int() returns 0 — no key-check needed print(dict(counts)) # {'to': 2, 'be': 2, 'or': 1, 'not': 1}
setup added so this can run · defines defaultdict
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def defaultdict(*_a, **_kw): print('-> defaultdict() called') return _AutoMock('defaultdict()')
For pure counting, Counter is still nicer. defaultdict(int) shines when the value transforms beyond a simple += 1.
3. deque — Fast at Both Ends
A list is great for indexing and appending. Pop from the front (list.pop(0)) and it's O(n) — Python has to shift every remaining element. A deque ("deck") is O(1) at both ends.
from collections import deque queue = deque(["a", "b", "c"]) queue.append("d") # right side queue.appendleft("z") # left side print(queue) # deque(['z', 'a', 'b', 'c', 'd']) print(queue.popleft()) # 'z' — FIFO queue print(queue.pop()) # 'd' — LIFO stack
Use cases:
- FIFO queues — producer/consumer patterns.
- Sliding windows — pass
maxlenand oldest items drop off automatically:
recent = deque(maxlen=3) for n in [1, 2, 3, 4, 5]: recent.append(n) print(list(recent)) # [1] → [1,2] → [1,2,3] → [2,3,4] → [3,4,5]
setup added so this can run · defines deque
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def deque(*_a, **_kw): print('-> deque() called') return _AutoMock('deque()')
- Tail-of-log — keep the last N lines without holding the whole file in memory.
If you ever find yourself writing lst.pop(0) in a hot loop, swap to deque.
4. namedtuple — Tuples With Field Names
A tuple is a great lightweight record — until you find yourself writing point[0] and point[1] and forgetting which is which. namedtuple keeps the immutability and the tuple-ness, but adds names:
from collections import namedtuple Point = namedtuple("Point", ["x", "y"]) p = Point(3, 4) print(p.x, p.y) # 3 4 print(p[0], p[1]) # 3 4 — still indexable print(p) # Point(x=3, y=4)
It's hashable, immutable, and unpacks normally:
x, y = p # tuple unpacking works points = {Point(0, 0): "origin"} # works as a dict key
setup added so this can run · defines p, Point
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) p = _AutoMock('p') def Point(*_a, **_kw): print('-> Point() called') return _AutoMock('Point()')
Modern preference: use typing.NamedTuple for the class-based syntax with type hints. Recommend this in any new code:
from typing import NamedTuple class Point(NamedTuple): x: float y: float label: str = "" # defaults supported p = Point(3, 4, "origin") print(p.label) # origin
Rule of thumb: NamedTuple for ultra-light, immutable value objects (coordinates, RGB colours, records read from a CSV). Use @dataclass(frozen=True) when you need richer behaviour — methods, mutation, __post_init__ validation, comparison overrides.
5. ChainMap — Layered Lookups
ChainMap searches multiple dicts in order — perfect for layered config (defaults → file → environment → CLI args):
from collections import ChainMap defaults = {"host": "localhost", "port": 8000, "debug": False} env = {"port": 9000} cli = {"debug": True} config = ChainMap(cli, env, defaults) # leftmost wins print(config["host"]) # 'localhost' (from defaults) print(config["port"]) # 9000 (env beats defaults) print(config["debug"]) # True (cli beats env)
Writes go to the first map only — the underlying dicts aren't merged or mutated:
config["host"] = "remote" print(cli) # {'debug': True, 'host': 'remote'} print(defaults) # unchanged
setup added so this can run · defines config, cli, defaults
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) config = _AutoMock('config') cli = _AutoMock('cli') defaults = _AutoMock('defaults')
It's a lightweight alternative to merging three dicts every time you need a config value.
6. OrderedDict — Mostly Obsolete
Before Python 3.7, regular dict made no order guarantees. OrderedDict preserved insertion order. Since 3.7, dict preserves insertion order in the language spec — so OrderedDict is mostly historical.
Two methods make it still useful:
from collections import OrderedDict od = OrderedDict([("a", 1), ("b", 2), ("c", 3)]) od.move_to_end("a") # move existing key to the end print(od) # OrderedDict([('b', 2), ('c', 3), ('a', 1)]) od.move_to_end("c", last=False) # ...or to the front print(od) # OrderedDict([('c', 3), ('b', 2), ('a', 1)]) print(od.popitem(last=False)) # ('c', 3) — pop from the front
These are handy for LRU-cache style structures. For everything else, reach for plain dict.
7. Which Container, When?
| Need | Reach for |
|---|---|
| Count occurrences of hashable things | Counter |
| Group items by key into lists/sets | defaultdict(list / set) |
| FIFO queue or sliding window | deque(maxlen=...) |
| Lightweight immutable record | typing.NamedTuple |
| Record with methods, defaults, mutation | @dataclass |
| Layered config lookup | ChainMap |
| LRU-style "move recent to end" | OrderedDict.move_to_end |
| Anything else | plain dict / list |
When in doubt, write it with plain types first. Swap in a specialized container only when the pattern is obvious or the performance matters.
Common Mistakes
dict.setdefault(k, []).append(v)everywhere — works, butdefaultdict(list)is shorter and faster. Reservesetdefaultfor one-off cases.Counter.most_common()without ann— returns all items sorted, which is wasteful if you only want the top 5. Passn.dequewithoutmaxlenwhen you wanted a sliding window — it'll grow unbounded. Themaxlenis the whole point of the sliding-window pattern.- Treating
namedtuplelike a mutable class — it's immutable. Trying to assignp.x = 5raisesAttributeError. For state that changes, use adataclass. - Reaching for
OrderedDictwhen plaindictworks — modern Python preserves insertion order natively. Only useOrderedDictif you needmove_to_endorpopitem(last=False). - Forgetting
Counterkeys can be negative after subtraction —Counterarithmetic with+drops zero/negative counts, but barec["x"] -= 1leaves negatives. Use+unary_counterto clean them up:+cdrops zero/negative.
🎯 Your Turn — Word Counter and Rolling Average
Two small functions, both of which use containers you just met.
Part 1: top_words(text, n=10) returns the top-n most common words in text. Lowercase, only "word characters" (letters, digits, underscore — use re.findall(r"\w+", ...)), no stopword filtering needed.
Part 2: rolling_average(stream, window=5) is a generator that consumes an iterable of numbers and yields the running average over the last window items. For the first window-1 items, yield the partial average (average of what you have so far).
import re from collections import Counter, deque def top_words(text, n=10): """Return list of (word, count) for the n most common words.""" # TODO 1: tokenize with re.findall(r"\w+", text.lower()) # TODO 2: Counter the tokens # TODO 3: return .most_common(n) pass def rolling_average(stream, window=5): """Yield the running mean over the last `window` items from `stream`.""" # TODO 1: deque with maxlen=window # TODO 2: for each item, append, then yield sum(buf) / len(buf) pass # Tests text = "The rain in Spain falls mainly on the plain. The plain is plain." print(top_words(text, 3)) # Expected: [('the', 3), ('plain', 3), ('in', 1)] (order between ties may vary) avgs = list(rolling_average([10, 20, 30, 40, 50], window=3)) print(avgs) # Expected: [10.0, 15.0, 20.0, 30.0, 40.0] # └─ 10/1 (10+20)/2 (10+20+30)/3 (20+30+40)/3 (30+40+50)/3
Hint 1 — Tokenizing
re.findall(r"\w+", text.lower()) returns a list of all word-character runs, lowercased. Punctuation and whitespace are skipped automatically.
Hint 2 — Sliding mean
Adeque(maxlen=window) automatically drops the oldest item when you append beyond maxlen. So sum(buf) / len(buf) always gives the average of the most recent ≤ window items, even before the window is full.
Show full solution
import re from collections import Counter, deque def top_words(text, n=10): tokens = re.findall(r"\w+", text.lower()) return Counter(tokens).most_common(n) def rolling_average(stream, window=5): buf = deque(maxlen=window) for item in stream: buf.append(item) yield sum(buf) / len(buf) # Tests text = "The rain in Spain falls mainly on the plain. The plain is plain." print(top_words(text, 3)) # [('the', 3), ('plain', 3), ('in', 1)] print(list(rolling_average([10, 20, 30, 40, 50], window=3))) # [10.0, 15.0, 20.0, 30.0, 40.0]
Five lines of real logic across both functions. That's the point of collections — push the loop-counter bookkeeping into the container and let your code state the intent.
A more numerically stable version of rolling_average would maintain a running sum and add/subtract on each step (O(1) per yield instead of O(window)). For small windows the difference is negligible; for window sizes in the thousands, optimise.
What You Learned
Counter(iterable)— tally hashables;most_common(n); arithmetic (+ - & |) for combining tallies.defaultdict(factory)— auto-create missing values;list/set/intare the common factories.deque— O(1) at both ends;maxlengives you a sliding window for free.namedtuple/typing.NamedTuple— immutable records with field names; prefer the typed class form. Use@dataclasswhen you need behaviour.ChainMap— layered lookup across multiple dicts; ideal for cascading configs.OrderedDict— historical, butmove_to_endandpopitem(last=False)keep it relevant for LRU patterns.- The decision table above is worth memorising — picking the right container is half of writing clean Python.
Next: itertools — the iteration toolbelt that pairs perfectly with these containers (chain, groupby, islice, accumulate, combinations).
Practice this
on practicepython.inShort exercises that run in your browser and tell you what your code actually did, not just whether a test passed.