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.
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.
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
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 countsdefaultdict 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
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 → 0namedtuple 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
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)
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.
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.
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) # unchangedSubclass 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.
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)
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.
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