list, dict, and set cover most needs, but the collections module has purpose-built structures for four extremely common patterns: counting, grouping, lightweight records, and double-ended queues. Reaching for these instead of reimplementing their logic by hand is a clear signal of Python fluency.
1Counter: Frequency Counting Without Manual Bookkeeping
The manual pattern counts = {}, then counts[word] = counts.get(word, 0) + 1 for every item, is common enough that collections.Counter exists specifically to replace it: Counter(words) (given any iterable of hashable items) builds the exact same frequency mapping in one call, as a dict subclass with a few extra conveniences layered on top. .most_common(n) returns the n most frequent items as (item, count) tuples, sorted descending by count ā a sort-and-slice operation you'd otherwise write by hand every time.
Counter also supports arithmetic between counters ā counter_a + counter_b adds corresponding counts together, counter_a - counter_b subtracts them (dropping any resulting non-positive counts) ā which is genuinely useful for comparing two frequency distributions, like word counts across two different documents, without writing a manual merge loop.
Accessing a key that was never counted returns 0 rather than raising KeyError, exactly like a defaultdict(int) would ā Counter()["never_seen"] is 0, not an exception, which is precisely the right default behavior for a running count that might legitimately be zero for many possible keys.
from collections import Counter
words = "the quick brown fox the lazy dog the fox".split()
counts = Counter(words)
print(counts) # Counter({'the': 3, 'fox': 2, 'quick': 1, ...})
print(counts.most_common(2)) # [('the', 3), ('fox', 2)][('the', 3), ('fox', 2)]
2defaultdict: Eliminating the "if key not in dict" Check
Grouping items by some key ā building a dict mapping each department to a list of employee names, in the scene's example ā traditionally requires checking whether the key already exists before appending: if dept not in groups: groups[dept] = [], then groups[dept].append(name). defaultdict(list) removes that check entirely: accessing (or assigning to) any key that doesn't yet exist automatically calls the factory function passed to defaultdict ā here, list() ā to create a default value for that key first, then proceeds with the operation as normal.
The factory can be any zero-argument callable: defaultdict(list) for grouping into lists, defaultdict(int) for counting (functionally overlapping with what Counter provides, though Counter is more purpose-built for pure counting), defaultdict(set) for grouping into sets when duplicates should be automatically eliminated, or even a custom function for more elaborate default construction logic.
One behavior worth knowing precisely: defaultdict only creates the default value on access ā reading groups["NewDept"] (even without assigning anything) *does* create an empty list for "NewDept" as a side effect, which can be surprising if you're just checking membership. Use dept in groups (which does not trigger default creation) rather than groups[dept] when you genuinely just want to check presence without mutating the dict.
from collections import defaultdict
groups = defaultdict(list)
for name, dept in [("Ada", "Eng"), ("Grace", "Eng"), ("Alan", "Math")]:
groups[dept].append(name) # no 'if dept not in groups' check needed
print(dict(groups)) # {'Eng': ['Ada', 'Grace'], 'Math': ['Alan']}No membership check needed ā defaultdict handles missing keys
3namedtuple and deque: A Lightweight Record and an Efficient Double-Ended Queue
namedtuple("Point", ["x", "y"]) creates a new tuple subclass whose elements are accessible both positionally (p[0]) and by name (p.x), combining a plain tuple's memory efficiency and immutability with dramatically better readability than remembering 'index 0 is x, index 1 is y.' It predates @dataclass (covered in Modern Python) and remains useful specifically when you want tuple semantics ā unpacking (x, y = p), equality by value, hashability, immutability ā without the extra weight or __init__ boilerplate of even a @dataclass. typing.NamedTuple is the modern, type-annotated variant (class Point(NamedTuple): x: float; y: float), combining namedtuple's tuple-based lightness with proper type hints.
deque ("deck", double-ended queue) is a sequence type specifically optimized for adding and removing items from *either* end in constant time ā .append()/.pop() on the right, .appendleft()/.popleft() on the left, all O(1). A plain list is a contiguous array under the hood, so while appending/popping from the *end* is O(1), inserting or removing from the *front* (list.insert(0, x) or list.pop(0)) requires shifting every other element down, making it O(n) ā a real, measurable difference at scale for anything implementing a queue, a sliding window, or breadth-first search's frontier.
deque also accepts an optional maxlen parameter, turning it into a fixed-size, automatically-evicting circular buffer ā appending past maxlen silently drops the oldest item from the opposite end, a convenient built-in mechanism for 'keep only the last N items' patterns like a rolling log tail or a recent-history buffer, without manual trimming logic.
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
print(p.x, p.y) # 3 4 -- named access, not just p[0], p[1]
print(p) # Point(x=3, y=4)O(1) ā versus O(n) for list.pop(0)
4Step-by-Step Breakdown
If you've ever written 'if key not in dict: dict[key] = []' ā collections has a structure that eliminates that check entirely. Let's meet it and three others.
Counter counts hashable items in one line -- no manual dict-plus-if-not-in-check loop needed.
defaultdict eliminates the 'if key not in dict' check entirely -- missing keys get a default value automatically.
Checkpoint: What does groups[dept].append(name) do when dept is not yet a key in a defaultdict(list)?
- ādefaultdict automatically creates an empty list for that key, then appends to it
- āIt raises a KeyError, same as a plain dict would
namedtuple (and its modern typed cousin) gives you a lightweight, immutable record with named fields -- more readable than a plain tuple, lighter than a full class.
deque gives O(1) appends/pops from BOTH ends -- a plain list is O(n) for operations at the front.
Checkpoint: Why is deque preferred over a plain list for a queue where items are added and removed from the front frequently?
- ādeque provides O(1) operations at both ends, while list.pop(0) and list.insert(0, x) are O(n)
- ādeque uses less memory per item than a list
collections gives you specialized containers; itertools gives you specialized ways to combine and transform them without materializing intermediate lists.
Group Real Data with defaultdict. Finish group_by_department(): defaultdict eliminates the 'if key not in dict' check.
Level Up š
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Best Practices
Reach for Counter instead of manually building a frequency dict with .get(key, 0) + 1
Counter is purpose-built, more readable, and comes with .most_common() and counter arithmetic that a hand-rolled dict would require reimplementing.
Use deque instead of list for any queue-like structure with frequent operations at the front
list.pop(0) and list.insert(0, x) are O(n) and become a real performance problem at scale; deque provides the same operations at O(1) for both ends.
Frequent Bugs
Using defaultdict(list) for membership checking (if key in defaultdict_instance) via defaultdict_instance[key], accidentally creating empty entries for keys that were only ever checked, not intentionally added.
Use `key in some_defaultdict` for pure membership checks ā it does not trigger default-value creation, unlike accessing `some_defaultdict[key]` directly.
Real-World Examples
Grouping Log Entries by Severity Level Using defaultdict
A log analysis script needs to group thousands of parsed log entries by their severity level (INFO, WARNING, ERROR) for a summary report, without pre-declaring every possible severity level in advance.
from collections import defaultdict
logs_by_severity = defaultdict(list)
for entry in parsed_log_entries:
logs_by_severity[entry.severity].append(entry)
for severity, entries in logs_by_severity.items():
print(f"{severity}: {len(entries)} entries")