Python collections Module
The built-in dict, list, and tuple types cover most needs, but some problems ask for a container with extra behavior baked in: counting things, grouping items by key, giving tuple fields names, or getting fast operations at both ends of a sequence. Python’s collections module provides exactly these specialized container datatypes. They are drop-in alternatives to the built-ins, implemented for speed and correctness, and using them instead of hand-rolled equivalents makes your code shorter, faster, and easier to read.
Overview / How it works
The collections module is part of the standard library, so it needs no installation — only an import. Every type in it is either a subclass of a built-in type (with extra methods or altered behavior) or a wrapper implemented in C for performance. The most commonly used members are:
- Counter — a
dictsubclass for counting hashable objects; missing keys return0instead of raisingKeyError. - defaultdict — a
dictsubclass that auto-creates a default value for missing keys using a factory function. - OrderedDict — a
dictsubclass with extra methods for reordering; largely superseded since Python 3.7 made plaindictinsertion-ordered, but still useful for itsmove_to_end()method and its order-sensitive equality checks. - namedtuple — a factory function that builds lightweight, immutable classes where fields can be accessed by name as well as by index.
- deque (“deck”) — a double-ended queue with O(1) appends and pops from both ends, unlike a
list, where inserting or removing at the front is O(n). - ChainMap — groups several dictionaries (or other mappings) into a single view without copying them, useful for layered configuration (defaults, then overrides).
Internally, Counter and defaultdict are literally subclasses of dict written in Python, overriding a handful of methods (__missing__ for defaultdict, plus arithmetic and most_common() for Counter). deque, by contrast, is implemented in C as a doubly linked list of fixed-size blocks, which is why it gets O(1) performance at both ends while still supporting fast iteration. namedtuple generates real Python source code for a new class (you can inspect it via the tuple’s _source in older versions, or just trust that it behaves like a regular tuple with named accessors added as properties).
Syntax
from collections import Counter, defaultdict, namedtuple, deque, OrderedDict, ChainMap
Counter(iterable_or_mapping)
defaultdict(default_factory)
namedtuple(typename, field_names)
deque(iterable, maxlen=None)
OrderedDict(items)
ChainMap(*mappings)
| Type | Built from | Key feature |
|---|---|---|
Counter |
dict |
Counts elements; missing keys return 0 |
defaultdict |
dict |
Auto-creates values via a factory callable |
namedtuple |
tuple |
Named, immutable, positional fields |
deque |
doubly linked list | O(1) append/pop at both ends |
OrderedDict |
dict |
Order-aware equality, move_to_end() |
ChainMap |
list of mappings | Layered lookup across multiple dicts |
Examples
Example 1: Counting words with Counter
from collections import Counter
text = "the quick brown fox jumps over the lazy dog the fox runs"
word_counts = Counter(text.split())
print(word_counts)
print(word_counts.most_common(3))
print(word_counts["fox"])
print(word_counts["cat"])
Output:
Counter({'the': 3, 'fox': 2, 'quick': 1, 'brown': 1, 'jumps': 1, 'over': 1, 'lazy': 1, 'dog': 1, 'runs': 1})
[('the', 3), ('fox', 2), ('quick', 1)]
2
0
Counter(text.split()) tallies every word in one pass. Notice two things a plain dict would not give you for free: most_common(3) returns the three highest counts, sorted descending (ties keep insertion order), and word_counts["cat"] returns 0 instead of raising KeyError, because Counter overrides __missing__.
Example 2: Grouping data with defaultdict
from collections import defaultdict
students = [
("Alice", "A"),
("Bob", "B"),
("Charlie", "A"),
("Dana", "C"),
("Eve", "B"),
]
by_grade = defaultdict(list)
for name, grade in students:
by_grade[grade].append(name)
for grade in sorted(by_grade):
print(f"{grade}: {by_grade[grade]}")
print(dict(by_grade))
Output:
A: ['Alice', 'Charlie']
B: ['Bob', 'Eve']
C: ['Dana']
{'A': ['Alice', 'Charlie'], 'B': ['Bob', 'Eve'], 'C': ['Dana']}
With a plain dict, appending to by_grade[grade] for a brand-new grade would raise KeyError unless you first checked if grade not in by_grade. defaultdict(list) removes that boilerplate: the first time a key is accessed, its __missing__ method calls the factory (list) to create an empty list, stores it under that key, and returns it — all before .append() even runs.
Example 3: Structured records with namedtuple
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p1 = Point(3, 4)
p2 = Point(x=1, y=2)
distance = ((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2) ** 0.5
print(p1)
print(p2)
print(f"Distance: {distance}")
print(p1._asdict())
p3 = p1._replace(x=10)
print(p3)
Output:
Point(x=3, y=4)
Point(x=1, y=2)
Distance: 2.8284271247461903
{'x': 3, 'y': 4}
Point(x=10, y=4)
Point is a real class generated on the fly: instances behave exactly like tuples (you could still write p1[0] to get 3), but p1.x is far more readable than remembering that index 0 means x. _asdict() converts an instance to a regular dict, and _replace() returns a new namedtuple with one or more fields changed — it does not mutate p1, because namedtuples, like ordinary tuples, are immutable.
Example 4: A bounded history with deque
from collections import deque
history = deque(maxlen=3)
for page in ["home", "products", "cart", "checkout", "confirmation"]:
history.append(page)
print(list(history))
history.appendleft("login")
print(list(history))
recent = history.pop()
print(f"Popped: {recent}, remaining: {list(history)}")
Output:
['home']
['home', 'products']
['home', 'products', 'cart']
['products', 'cart', 'checkout']
['cart', 'checkout', 'confirmation']
['login', 'cart', 'checkout']
Popped: checkout, remaining: ['login', 'cart']
Passing maxlen=3 turns the deque into a fixed-size sliding window: once it holds three items, each new append() silently discards the oldest item from the opposite end. appendleft() pushes onto the front in O(1) time — something a list cannot do without shifting every other element, an O(n) operation. This makes deque the natural choice for browsing history, undo buffers, and sliding-window algorithms.
Under the hood
Counteris a subclass ofdict; its constructor callsupdate(), which iterates the argument (a string, list, or another mapping) and increments counts. Arithmetic operators are overloaded:c1 + c2adds counts,c1 - c2subtracts (dropping zero or negative results), andc1 & c2/c1 | c2take the min/max count per key.defaultdict.__missing__(key)is called automatically bydict.__getitem__whenever a key is absent. It callsself.default_factory()with no arguments, stores the result underkey, and returns it. Ifdefault_factoryisNone, a missing key still raisesKeyErroras normal.namedtuple()dynamically builds and executes source code for a new class with__slots__ = (), so instances use no more memory than a plain tuple — there is no per-instance__dict__.dequestores elements in a doubly linked list of small fixed-size arrays (“blocks”), so appends/pops at either end just adjust pointers. The tradeoff: indexing into the middle,dq[500], is O(n), unlike alist‘s O(1) indexing.
Common Mistakes
Mistake 1: Passing a non-callable to defaultdict
defaultdict‘s argument must be something Python can call with zero arguments to produce a default value — a type like int, list, or set, or a zero-argument function. Passing a literal value instead raises TypeError:
from collections import defaultdict
counts = defaultdict(0) # TypeError: first argument must be callable or None
counts["a"] += 1
Use the type itself, not an instance of it, as the factory:
from collections import defaultdict
counts = defaultdict(int) # int() returns 0
counts["a"] += 1
print(counts["a"])
Mistake 2: Trying to mutate a namedtuple in place
Because namedtuples are immutable, assigning to a field directly fails:
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(1, 2)
p.x = 10 # AttributeError: can't set attribute
Use _replace() to get a new instance with the field changed, and rebind the name if you need to keep using it:
p = p._replace(x=10)
Best Practices
- Reach for
Counterwhenever you’re tallying occurrences instead of writing a manualif key in dictloop. - Use
defaultdict(list)ordefaultdict(set)for grouping instead ofdict.setdefault()calls scattered through a loop — it reads more clearly at the call site. - Prefer
namedtuple(ortyping.NamedTuplefor type-hinted fields) over plain tuples whenever a tuple’s meaning depends on remembering index order. - Choose
dequeoverlistwhenever you need to insert or remove from the front of a sequence, or want a capped, sliding-window buffer viamaxlen. - Don’t use
OrderedDictjust for insertion order on modern Python — plaindictalready preserves it since 3.7. Reach forOrderedDictspecifically when you needmove_to_end()or order-sensitive equality comparisons. - Import only what you use (
from collections import Counter) rather than importing the whole module, for clarity and slightly faster lookups.
Practice Exercises
- Given a list of email addresses, use
Counterto find which domain (the part after@) appears most often. - Use a
defaultdict(list)to group a list of(name, department)tuples into a dictionary mapping each department to a list of employee names. - Define a
namedtuplecalledBookwith fieldstitle,author, andyear. Create threeBookinstances and print them sorted byyearusingsorted()with akeyfunction.
Summary
collectionsprovides specialized container types built ondict,list, andtuplefor common patterns.Countercounts hashable items and never raisesKeyErrorfor missing keys.defaultdictauto-creates missing values with a factory callable, eliminating manual existence checks.namedtuplegives tuples named, immutable fields while staying as lightweight as a plain tuple.dequeoffers O(1) appends and pops at both ends, plus an optionalmaxlenfor sliding windows.OrderedDictandChainMapround out the module for order-sensitive dicts and layered mappings.
