🐍 Python Course · Stage 3 · Lesson 34 of 89 · collections — Containers
Course Overview →

🔵 Read Beginner and Intermediate tabs. The Expert tab has the internals — worth reading once you finish Stage 3.

← datetime — Dates & Times itertools — Iterators →
thecodex.expert · The Codex Family of Knowledge
Python stdlib

Python collections — Specialised Container Datatypes module

Counter, defaultdict, namedtuple, deque — the specialised data structures that solve common problems with less code.

Python 3.13 Standard Library Last verified:
Canonical Definition

The Python collections module provides specialised container datatypes beyond the built-in list, dict, and set — including Counter for tallying, defaultdict for auto-initialised dicts, namedtuple for named tuples, and deque for O(1) double-ended queues.

🟩 Beginner

Specialised containers for common patterns

What you'll learn: Counter for frequency counting and defaultdict for dictionaries with default values. These two eliminate a surprising amount of boilerplate code.

How to read this tab: Take any list of words or items you've been counting manually in a loop. Rewrite it as Counter(items).most_common(10). The feeling when you see how much code disappears is the lesson.

⏱ 30 min 📄 2 sections 🔶 Prerequisite: File I/O
One sentence

The collections module contains specialised data structures that go beyond Python's built-in list, dict, and set — each solving a common problem with less code and better performance.

Counter replaces the most common Python counting pattern. The old way: counts = {}; for word in words: counts[word] = counts.get(word, 0) + 1. The Counter way: counts = Counter(words). One line. Then counts.most_common(10) gives you the top 10 in frequency order. Counter also supports arithmetic: adding two Counters merges their counts.

Counter

Pythoncounter.py
from collections import Counter

# Count occurrences of elements
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
counts = Counter(words)
print(counts)                # Counter({'apple': 3, 'banana': 2, 'cherry': 1})
print(counts["apple"])       # 3
print(counts["unknown"])     # 0 — missing keys return 0, not KeyError

# most_common(n) — top n elements
print(counts.most_common(2)) # [('apple', 3), ('banana', 2)]

# Count characters in a string
char_counts = Counter("mississippi")
print(char_counts.most_common(3))   # [('s', 4), ('i', 4), ('p', 2)]

# Arithmetic
c1 = Counter(a=3, b=2)
c2 = Counter(a=1, b=4)
print(c1 + c2)   # Counter({'b': 6, 'a': 4})
print(c1 - c2)   # Counter({'a': 2})  — only positive counts
💡

defaultdict eliminates the "if key not in dict" pattern. Instead of: if key not in d: d[key] = []; d[key].append(value) — use d = defaultdict(list) then just d[key].append(value). The factory function (list, int, set, etc.) is called automatically when a missing key is accessed.

defaultdict

Pythondefaultdict.py
from collections import defaultdict

# defaultdict(factory) — auto-creates missing keys
word_positions = defaultdict(list)    # missing key → empty list

for i, word in enumerate("the quick brown fox jumps".split()):
    word_positions[word].append(i)   # no KeyError for new words

print(dict(word_positions))
# {'the': [0], 'quick': [1], 'brown': [2], 'fox': [3], 'jumps': [4]}

# Grouping
students = [("A", "Priya"), ("B", "Rahul"), ("A", "Anita"), ("B", "Zara")]
by_grade = defaultdict(list)
for grade, name in students:
    by_grade[grade].append(name)
print(dict(by_grade))   # {'A': ['Priya', 'Anita'], 'B': ['Rahul', 'Zara']}

# Counter: count with defaultdict(int)
counts = defaultdict(int)
for word in "the fox the fox fox".split():
    counts[word] += 1   # no KeyError — missing key → 0
💡

namedtuple vs dataclass: Both give you tuples/classes with named fields. Use namedtuple when you want the immutability and memory efficiency of a tuple but need readable field names. Use @dataclass when you need mutability, methods, or default values. dataclass is usually the better modern choice.

namedtuple and OrderedDict

Pythoncollections_misc.py
from collections import namedtuple, OrderedDict, deque

# namedtuple — tuple with named fields
Point = namedtuple("Point", ["x", "y"])
p = Point(10, 20)
print(p.x, p.y)   # 10 20
print(p[0])        # 10 — index access still works
print(p._asdict()) # {'x': 10, 'y': 20}
x, y = p           # unpacking still works

Employee = namedtuple("Employee", "name department salary")
emp = Employee("Priya", "Engineering", 75000)
print(emp.name)    # Priya

# OrderedDict — dict that remembers insertion order
# Note: Regular dict is ordered since Python 3.7
# OrderedDict still has move_to_end() and popitem(last=)
od = OrderedDict()
od["a"] = 1
od["b"] = 2
od.move_to_end("a")   # move to end
print(list(od))        # ['b', 'a']

# deque — O(1) insert/delete at both ends (unlike list which is O(n) at front)
q = deque([1, 2, 3])
q.appendleft(0)    # O(1) — add to front
q.popleft()        # O(1) — remove from front
q.append(4)        # O(1) — add to back
q.rotate(1)        # rotate right by 1: [4, 1, 2, 3]

# maxlen — sliding window
recent = deque(maxlen=3)    # keeps last 3 items
for x in range(6):
    recent.append(x)
print(list(recent))   # [3, 4, 5]

✅ Beginner tab complete — check your understanding

  • I can use Counter(list) to count frequencies in one line
  • I can use Counter.most_common(n) to get the n most frequent items
  • I can use defaultdict(list) to avoid KeyError when the key doesn't exist yet
  • I can use defaultdict(int) as a counter dict (auto-initialises to 0)

Continue to itertools →

🔵 Intermediate

deque, namedtuple, and OrderedDict

What you'll learn: deque for O(1) operations on both ends (faster than list for queues). namedtuple for tuples with readable field names. OrderedDict and when you still need it.

How to read this tab: Implement a queue with deque and test appendleft/popleft. Then compare with doing the same with a list and feel the difference.

⏱ 25 min 📄 2 sections 🔶 Prerequisite: Beginner tab
📎

ChainMap is perfect for layered configurations. Default settings → user settings → environment variables, where each layer overrides the previous. ChainMap looks up keys in each map in order. Updating through ChainMap only updates the first map — the layers beneath are preserved.

ChainMap

ChainMap links multiple dicts into a single view. Lookups search each map in order — the first one containing the key wins. Writes go to the first map. This is the mechanism behind Python's LEGB scope lookup — local vars, then enclosing, then global, then built-ins, all as separate dicts.

Pythonchainmap.py
from collections import ChainMap

defaults  = {"color": "blue",  "size": "medium", "style": "flat"}
overrides = {"color": "green", "size": "large"}

config = ChainMap(overrides, defaults)
print(config["color"])  # green — from overrides
print(config["style"])  # flat  — from defaults
print(dict(config))     # {"color":"green","size":"large","style":"flat"}

# ChainMap is how Python implements nested scopes
# Updating the first map only
config["color"] = "red"
print(overrides)   # {'color': 'red', 'size': 'large'}
print(defaults)    # unchanged
📎

Subclass UserDict, not dict. If you want a custom dictionary with special behaviour on item access or assignment, subclassing dict directly has gotchas — many dict methods don't call __getitem__ or __setitem__, bypassing your overrides. UserDict routes everything through those methods properly.

UserDict, UserList, UserString

These are wrapper classes that make it easy to subclass dict, list, and str without running into the pitfalls of subclassing built-ins directly (built-in methods sometimes bypass overridden methods). Subclassing UserDict instead of dict ensures all dict operations go through your overridden methods.

Commonly confused
deque is not a list with O(1) front operations. deque does not support O(1) random access by index. deque[n] is O(n) (traversal from nearest end). Use deque when you need O(1) insertions/deletions at both ends (queues, sliding windows). Use list when you need O(1) random access.
namedtuple is still a tuple. It is immutable. You cannot set p.x = 5. For mutable named-field data classes, use @dataclass (Python 3.7+) or namedtuple._replace(x=5) to create a new copy.
OrderedDict is not obsolete in Python 3.7+. Regular dicts are insertion-ordered since 3.7, but OrderedDict still has move_to_end(), popitem(last=False), and equality that considers order — two OrderedDicts with the same keys in different order are not equal, while two regular dicts would be equal.

✅ Intermediate tab complete — check your understanding

  • I know why deque is faster than list for appendleft/popleft operations
  • I can create a namedtuple and access fields by name instead of index
  • I know that regular dict maintains insertion order in Python 3.7+ (so OrderedDict is rarely needed)

Continue to itertools →

🔴 Expert

ChainMap, UserDict, and collections.abc

What you'll learn: ChainMap for layered lookups (useful for config hierarchies). UserDict/UserList as base classes for custom container implementations. The abstract base classes in collections.abc.

How to read this tab: Read when you need to create a custom dict-like or list-like class, or implement a layered configuration system.

⏱ 20 min 📄 2 sections 🔶 Prerequisite: After Stage 3
📎

collections.abc defines the interfaces for Python's container types. Mapping, Sequence, MutableMapping, MutableSequence, Iterator, Iterable, Hashable, Callable — these ABCs define what each type must implement. Use isinstance(obj, Mapping) to check if something behaves like a dict without requiring it to be a dict.

Abstract Base Classes in collections.abc

The collections.abc module (part of collections) defines Abstract Base Classes (ABCs) for containers: Iterable, Iterator, Sequence, MutableSequence, Mapping, MutableMapping, Set, MutableSet. These enable isinstance() checks for protocol compliance and serve as base classes. A class implementing __iter__ and __next__ is automatically an Iterator (via virtual subclass registration). This is how isinstance([], Sequence) returns True even though list does not explicitly inherit from Sequence.

✅ Expert tab complete

  • I can use ChainMap for layered lookups (e.g., defaults overridden by user settings)
  • I know to subclass UserDict instead of dict when creating custom mapping types

Continue to itertools →

Sources

1
Python Standard Library — collections. docs.python.org/3/library/collections.html.
2
Python Standard Library — collections.abc. docs.python.org/3/library/collections.abc.html.
3
PEP 3119 — Introducing Abstract Base Classes. peps.python.org/pep-3119/.
Source confidence: High Last verified: Primary source: docs.python.org/3/library/collections.html

Sources

1
Python Standard Library — collections. docs.python.org/3/library/collections.html.
2
Python Standard Library — collections.abc. docs.python.org/3/library/collections.abc.html.