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 dict subclass for counting hashable objects; missing keys return 0 instead of raising KeyError.
  • defaultdict — a dict subclass that auto-creates a default value for missing keys using a factory function.
  • OrderedDict — a dict subclass with extra methods for reordering; largely superseded since Python 3.7 made plain dict insertion-ordered, but still useful for its move_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

  • Counter is a subclass of dict; its constructor calls update(), which iterates the argument (a string, list, or another mapping) and increments counts. Arithmetic operators are overloaded: c1 + c2 adds counts, c1 - c2 subtracts (dropping zero or negative results), and c1 & c2 / c1 | c2 take the min/max count per key.
  • defaultdict.__missing__(key) is called automatically by dict.__getitem__ whenever a key is absent. It calls self.default_factory() with no arguments, stores the result under key, and returns it. If default_factory is None, a missing key still raises KeyError as 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__.
  • deque stores 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 a list‘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 Counter whenever you’re tallying occurrences instead of writing a manual if key in dict loop.
  • Use defaultdict(list) or defaultdict(set) for grouping instead of dict.setdefault() calls scattered through a loop — it reads more clearly at the call site.
  • Prefer namedtuple (or typing.NamedTuple for type-hinted fields) over plain tuples whenever a tuple’s meaning depends on remembering index order.
  • Choose deque over list whenever you need to insert or remove from the front of a sequence, or want a capped, sliding-window buffer via maxlen.
  • Don’t use OrderedDict just for insertion order on modern Python — plain dict already preserves it since 3.7. Reach for OrderedDict specifically when you need move_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 Counter to 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 namedtuple called Book with fields title, author, and year. Create three Book instances and print them sorted by year using sorted() with a key function.

Summary

  • collections provides specialized container types built on dict, list, and tuple for common patterns.
  • Counter counts hashable items and never raises KeyError for missing keys.
  • defaultdict auto-creates missing values with a factory callable, eliminating manual existence checks.
  • namedtuple gives tuples named, immutable fields while staying as lightweight as a plain tuple.
  • deque offers O(1) appends and pops at both ends, plus an optional maxlen for sliding windows.
  • OrderedDict and ChainMap round out the module for order-sensitive dicts and layered mappings.