Python `collections` Module Explained

Learn how to use Python's collections module for specialized container types like Counter, defaultdict, OrderedDict, deque, and namedtuple.

7 min read

The collections module in the Python standard library provides specialized container types that extend Python's built-in list, dict, tuple, and set. These containers handle common patterns like counting items, grouping values, managing queues, and creating lightweight record types, without writing the same boilerplate over and over.

pythonpython
from collections import Counter
 
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
counts = Counter(words)
print(counts)                      # Counter({'apple': 3, 'banana': 2, 'cherry': 1})
print(counts.most_common(2))  # [('apple', 3), ('banana', 2)]

Counter(words) counts how many times each word appears in the list, building a dictionary-like mapping from word to count in a single call. .most_common(2) returns the two most frequent items, sorted by count, along with their totals.

Counter: counting hashable items

Counting occurrences by hand usually means a dict plus a few lines of boilerplate for missing keys. Counter is a dictionary subclass for counting, and you can pass it any iterable directly.

pythonpython
from collections import Counter
 
text = "mississippi"
counts = Counter(text)
print(counts)          # Counter({'i': 4, 's': 4, 'p': 2, 'm': 1})
print(counts["i"])   # 4
print(counts["z"])  # 0

Missing keys return 0 instead of raising KeyError, which is convenient in loops where you increment a count before knowing whether the key has been seen before. Counter instances also support arithmetic operators directly, similar to sets and vectors:

pythonpython
from collections import Counter
 
a = Counter("abracadabra")
b = Counter("alakazam")
 
print(a + b)
print(a - b)
print(a & b)
print(a | b)

Each operator combines the two counters differently, from summing every count to keeping only the shared minimums, and the output below shows all four results side by side for the same pair of counters:

texttext
Counter({'a': 9, 'b': 2, 'r': 2, 'c': 1, 'd': 1, 'l': 1, 'k': 1, 'z': 1, 'm': 1})
Counter({'b': 2, 'r': 2, 'a': 1, 'c': 1, 'd': 1})
Counter({'a': 4})
Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1, 'l': 1, 'k': 1, 'z': 1, 'm': 1})
  • sums counts. - subtracts (drops non-positive results). & takes minimums (intersection). | takes maximums (union).

defaultdict: automatic default values

Grouping items by key normally means checking whether the key already exists before appending to it. defaultdict returns a default value when you access a missing key, instead of raising KeyError, which removes that check entirely.

pythonpython
from collections import defaultdict
 
students = [("Maya", "A"), ("Devin", "B"), ("Maya", "B"), ("Priya", "A")]
 
grades = defaultdict(list)
for name, grade in students:
    grades[name].append(grade)
 
print(dict(grades))  # {'Maya': ['A', 'B'], 'Devin': ['B'], 'Priya': ['A']}

defaultdict(list) creates a new empty list for every new key. No if name not in grades check is needed. Pass any callable that takes no arguments:

pythonpython
counts = defaultdict(int)
positions = defaultdict(lambda: (0, 0))

defaultdict(int) returns 0 for missing keys. defaultdict(lambda: (0, 0)) returns a tuple for missing keys.

deque: fast double-ended queue

deque (pronounced "deck") supports fast appends and pops from both ends. It is ideal for queues, stacks, and sliding windows.

pythonpython
from collections import deque
 
history = deque(maxlen=3)
history.append("first")
history.append("second")
history.append("third")
history.append("fourth")
 
print(history)  # deque(['second', 'third', 'fourth'], maxlen=3)

maxlen=3 keeps only the last three items. When a fourth is added, the oldest is automatically removed. This is perfect for log buffers, recent-item lists, and sliding window calculations.

Operations at both ends are O(1), which is the whole reason deque exists instead of just using a plain list for this kind of work:

pythonpython
from collections import deque
 
queue = deque(["a", "b", "c"])
queue.append("d")
queue.appendleft("z")
print(queue)
 
print(queue.pop())
print(queue.popleft())
print(queue)

The print after the appends shows the new order, and the two pops remove one item from each end before the final print confirms what remains:

texttext
deque(['z', 'a', 'b', 'c', 'd'])
d
z
deque(['a', 'b', 'c'])

Use deque instead of list when you add or remove from the beginning. list.pop(0) is O(n); deque.popleft() is O(1).

namedtuple: lightweight record types

namedtuple creates tuple subclasses with named fields. They are immutable, memory-efficient, and support both attribute and index access.

pythonpython
from collections import namedtuple
 
Point = namedtuple("Point", ["x", "y"])
p = Point(10, 20)
 
print(p.x, p.y)      # 10 20
print(p[0], p[1])   # 10 20
x, y = p
print(x, y)              # 10 20

Attribute access, index access, and unpacking all return the same values, since a namedtuple is still a plain tuple underneath. Named tuples work everywhere tuples work: indexing, unpacking, and hashing. They are great for representing coordinates, database rows, configuration records, and any structured data that should not change after creation.

pythonpython
from collections import namedtuple
 
Student = namedtuple("Student", ["name", "score", "grade"])
records = [
    Student("Maya", 92, "A"),
    Student("Devin", 85, "B"),
]
for s in records:
    print(f"{s.name}: {s.score}")

The loop prints one line per student, reading each field by name instead of by tuple position, which keeps the code readable even as more fields get added later:

texttext
Maya: 92
Devin: 85

OrderedDict: order-preserving dictionary

Since Python 3.7, regular dict maintains insertion order. OrderedDict still has value for specific use cases like LRU caches and reordering operations.

pythonpython
from collections import OrderedDict
 
cache = OrderedDict([("a", 1), ("b", 2), ("c", 3)])
cache.move_to_end("a")
print(cache)
 
cache.popitem(last=False)
print(cache)

The first print shows "a" moved to the end after move_to_end(), and the second print shows the dict after popitem(last=False) removes whichever key is now first:

texttext
OrderedDict({'b': 2, 'c': 3, 'a': 1})
OrderedDict({'c': 3, 'a': 1})

move_to_end() moves a key to the front or back. popitem(last=False) removes the first item. These operations make OrderedDict useful for implementing LRU caches and ordered sets.

ChainMap: group multiple dictionaries

ChainMap groups multiple dictionaries into a single mapping. Lookups search each dictionary in order.

pythonpython
from collections import ChainMap
 
defaults = {"theme": "light", "font_size": 14, "language": "en"}
user = {"theme": "dark", "font_size": 16}
 
settings = ChainMap(user, defaults)
print(settings["theme"])
print(settings["font_size"])
print(settings["language"])

theme and font_size resolve from user since it comes first in the ChainMap, while language falls through to defaults since user does not define it:

texttext
dark
16
en

settings["theme"] finds the value in user first because it is the first mapping. settings["language"] falls through to defaults. This pattern is useful for configuration layering, where command-line arguments override config files, which override defaults.

Comparison of container types

TypeUse caseMutability
CounterCount items, find most commonMutable
defaultdictDict with automatic default valuesMutable
dequeFast queue/stack at both endsMutable
namedtupleLightweight immutable recordsImmutable
OrderedDictDict with reordering operationsMutable
ChainMapLayered lookup across dictsMutable (underlying dicts)

Common mistakes

Using list as a queue. list.pop(0) shifts all remaining elements, making it O(n). Use deque with .popleft() for O(1) queue behavior.

Using Counter for arithmetic without checking the result. Counter subtraction drops non-positive counts. If you need negative counts, use a regular defaultdict(int) instead.

Confusing namedtuple with dataclass. namedtuple is immutable, lighter, and tuple-compatible. dataclass is mutable by default, supports methods and type hints, and is better for data models that change. See dataclasses for the alternative.

Modifying a ChainMap's first mapping by accident. Mutations to a ChainMap only affect the first mapping. If you intend to modify a later mapping, access it directly by index: chain.maps[1]["key"] = value.

Rune AI

Rune AI

Key Insights

  • Counter counts hashable items and provides .most_common() and arithmetic operations.
  • defaultdict returns a default value for missing keys, eliminating KeyError checks.
  • deque supports fast O(1) appends and pops from both ends.
  • namedtuple creates lightweight, immutable record types with named fields.
  • OrderedDict maintains insertion order (useful for LRU caches and ordered mappings).
  • ChainMap groups multiple dictionaries into a single view.
RunePowered by Rune AI

Frequently Asked Questions

When should I use defaultdict instead of a regular dict?

Use `defaultdict` when you often need to handle missing keys with a default value. For example, counting items, grouping values by key, or building nested structures. It eliminates repeated `if key not in dict` checks and makes the code cleaner.

What is the difference between deque and list?

A `deque` (double-ended queue) supports fast O(1) appends and pops from both ends. A list is fast at the end but O(n) at the beginning. Use `deque` for queues, stacks, and sliding windows. Use `list` for random access and when you only modify the end.

Should I use namedtuple or a dataclass?

Use `namedtuple` when you need a lightweight, immutable record type with tuple-like behavior (indexing, unpacking). Use `dataclasses.dataclass` when you need mutable attributes, default values, methods, or type hints. `namedtuple` is more memory-efficient; `dataclass` is more flexible.

Conclusion

The collections module fills gaps in Python's built-in container types. Use Counter for frequency counting, defaultdict for automatic default values, deque for fast double-ended operations, and namedtuple for lightweight record types. Each is a drop-in improvement over a hand-rolled equivalent.