collections
The collections module supplies specialized container types -- Counter, defaultdict, deque, namedtuple, and more -- that solve common data-structure needs more directly than plain dict, list, and tuple.
The built-in dict, list, and tuple cover most needs, but some patterns come up so often that the standard library provides purpose-built alternatives. The collections module bundles these: a counting dict, a dict with automatic defaults, a fast double-ended queue, a lightweight named record, a layered mapping view, and base classes for subclassing built-in container behavior.
What Is the collections Module
collections is a standard-library module providing specialized, general-purpose container datatypes as alternatives to Python's built-in dict, list, set, and tuple. Each type solves one recurring pattern well, rather than being a general-purpose replacement -- you reach for them when the built-in's shape doesn't quite fit the problem.
from collections import Counter, defaultdict, deque, namedtuple, OrderedDict, ChainMapCounter: Counting Hashable Objects
Counter is a dict subclass for counting hashable items. Its constructor accepts an iterable (counting occurrences) or keyword arguments directly. Missing keys return 0 instead of raising KeyError, and it supports set-like arithmetic (+, -, &, |) for combining counts.
most_common(n) returns the n highest-count items as (item, count) tuples, sorted descending -- one of the most common reasons to reach for Counter.
words = ['red', 'blue', 'red', 'green', 'blue', 'red']
counts = Counter(words)
print(counts) # Counter({'red': 3, 'blue': 2, 'green': 1})
print(counts['red']) # 3
print(counts['purple']) # 0 -- no KeyError, unlike a plain dict
print(counts.most_common(2)) # [('red', 3), ('blue', 2)]
a = Counter(a=3, b=1)
b = Counter(a=1, b=2)
print(a + b) # Counter({'a': 4, 'b': 3})defaultdict: Automatic Missing-Key Defaults
defaultdict(default_factory) is a dict subclass that calls default_factory() to create a value automatically the first time a missing key is accessed, instead of raising KeyError. This removes the need for the common 'check if key exists, then initialize' pattern.
default_factory is any zero-argument callable -- list, int, set, or your own function -- called fresh each time a genuinely missing key is looked up, and only then.
Accessing a missing key on a defaultdict inserts that key with the default value as a side effect. Use dict.get() or 'in' checks instead of a defaultdict when you want to inspect without inserting.
groups = defaultdict(list)
for name, team in [('Ana', 'red'), ('Bo', 'blue'), ('Cy', 'red')]:
groups[team].append(name)
print(dict(groups)) # {'red': ['Ana', 'Cy'], 'blue': ['Bo']}
tally = defaultdict(int)
for ch in 'banana':
tally[ch] += 1
print(dict(tally)) # {'b': 1, 'a': 3, 'n': 2}deque: A Double-Ended Queue
deque (pronounced 'deck') is a double-ended queue supporting O(1) appends and pops from both ends, unlike list, whose insert(0, x) and pop(0) are O(n) because every remaining element must shift. Use deque for queues, stacks, sliding windows, and any pattern that adds/removes from the front.
maxlen=3 turns the deque into a fixed-size sliding window: once full, appending on one end automatically discards from the other.
deque supports indexing (q[0]) but random-access indexing is O(n) in the middle, unlike list -- it's optimized for the ends, not for arbitrary positions.
from collections import deque
q = deque([1, 2, 3])
q.append(4) # add to right
q.appendleft(0) # add to left -- O(1), unlike list.insert(0, x)
print(q) # deque([0, 1, 2, 3, 4])
q.pop() # remove from right
q.popleft() # remove from left
print(q) # deque([1, 2, 3])
recent = deque(maxlen=3) # bounded deque -- auto-discards oldest
for i in range(5):
recent.append(i)
print(recent) # deque([2, 3, 4], maxlen=3)namedtuple: Lightweight Immutable Records
namedtuple(typename, field_names) generates a tuple subclass whose elements can be accessed by name as well as by index. Instances are immutable, hashable (when all fields are hashable), and lightweight -- no per-instance __dict__, just like a plain tuple, since fields are stored positionally.
namedtuple predates the dataclasses module (Python 3.7) and is still useful for simple, truly immutable records where tuple-like behavior (unpacking, indexing, hashability) matters as much as named access. See the dataclasses topics for a more feature-rich, mutable-by-default alternative.
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
p = Point(3, 4)
print(p.x, p.y) # 3 4 -- attribute access
print(p[0], p[1]) # 3 4 -- still a tuple, index access works too
print(p) # Point(x=3, y=4) -- readable repr
x, y = p # unpacking works like any tupleOrderedDict: Historical Context
Before Python 3.7, plain dict did not guarantee insertion order, so OrderedDict existed specifically to provide that guarantee. Since 3.7, plain dict preserves insertion order as a language guarantee, which removed OrderedDict's original reason for existing -- but it still offers two things plain dict does not: move_to_end() to reposition a key, and an __eq__ that is order-sensitive (two OrderedDicts with the same items in different orders compare unequal, whereas two plain dicts compare equal regardless of order).
For new code that only needs insertion order (not move_to_end or order-sensitive equality), a plain dict is simpler and just as ordered -- reach for OrderedDict specifically for those two extra behaviors.
from collections import OrderedDict
od = OrderedDict(a=1, b=2, c=3)
od.move_to_end('a')
print(od) # OrderedDict({'b': 2, 'c': 3, 'a': 1})
print(OrderedDict(a=1, b=2) == OrderedDict(b=2, a=1)) # False -- order matters
print({'a': 1, 'b': 2} == {'b': 2, 'a': 1}) # True -- plain dict ignores orderChainMap: Layered Lookup Across Mappings
ChainMap(*mappings) groups multiple dicts (or dict-like mappings) into a single view. Lookups check each mapping in order and return the first match; writes go to the first mapping only. It's a common tool for layering configuration -- command-line overrides, then environment variables, then defaults -- without merging them into a new dict.
ChainMap avoids copying: unlike {defaults, overrides}, the underlying dicts stay separate and live, so later changes to overrides are reflected without rebuilding anything.
defaults = {'color': 'blue', 'size': 'M'}
overrides = {'size': 'L'}
config = ChainMap(overrides, defaults)
print(config['size']) # 'L' -- found in overrides first
print(config['color']) # 'blue' -- falls through to defaults
config['size'] = 'XL'
print(overrides) # {'size': 'XL'} -- writes go to the first mappingUserDict, UserList, UserString: Subclassing-Friendly Wrappers
Subclassing dict, list, or str directly can behave unexpectedly because some of their C-implemented methods don't consistently call your overridden methods (CPython implementation detail: internal methods sometimes bypass Python-level method dispatch for speed). UserDict, UserList, and UserString wrap the underlying data in a plain attribute (self.data) and implement all the container behavior in ordinary Python methods, so overriding one method reliably affects everything that uses it.
Use these when you need dict-like, list-like, or str-like behavior with custom overrides; for simple composition without needing 'is-a' container semantics, wrapping a plain dict/list as an attribute is often simpler still.
from collections import UserDict
class CaseInsensitiveDict(UserDict):
def __setitem__(self, key, value):
super().__setitem__(key.lower(), value)
def __getitem__(self, key):
return super().__getitem__(key.lower())
d = CaseInsensitiveDict()
d['Name'] = 'Ada'
print(d['name']) # 'Ada' -- lookup is normalized reliablyChoosing the Right Collection
Need Reach for
count occurrences of items Counter
auto-initialize missing dict keys defaultdict
fast append/pop at both ends deque
immutable record with named fields namedtuple (or a frozen dataclass)
reposition keys / order-sensitive eq OrderedDict
layered lookup over several dicts ChainMap
subclass dict/list/str reliably UserDict / UserList / UserStringComplete Example: Word Frequency Report
Counter answers 'how many of each', defaultdict(list) answers 'group these by some key' -- a pairing that shows up constantly in text and log processing.
from collections import Counter, defaultdict
text = "the quick brown fox jumps over the lazy dog the fox runs"
words = text.split()
freq = Counter(words)
by_length = defaultdict(list)
for w in freq:
by_length[len(w)].append(w)
print(freq.most_common(2)) # [('the', 3), ('fox', 2)]
print(dict(sorted(by_length.items())))Key Takeaways
Counter, defaultdict, and deque are dict/list subclasses (or close relatives) built for counting, auto-defaulting, and fast double-ended access.
namedtuple gives you attribute access on top of ordinary tuple behavior -- immutable, hashable, and lightweight.
Since Python 3.7, plain dict already preserves insertion order; reach for OrderedDict only for move_to_end() or order-sensitive equality.
ChainMap layers multiple mappings for lookup without copying or merging them.
UserDict/UserList/UserString are the reliable way to subclass container behavior, because they route everything through overridable Python methods.
Accessing a missing key on a defaultdict inserts it as a side effect -- use 'in' or .get() when you only want to check, not create.
counts = {} for w in words: if w not in counts: counts[w] = 0 counts[w] += 1 from collections import Counter counts = Counter(words)
Counter replaces the manual 'check then initialize' pattern with a single constructor call, and adds most_common() for free.
d = {} for k, v in pairs: if k not in d: d[k] = [] d[k].append(v) d = defaultdict(list) for k, v in pairs: d[k].append(v) defaultdict eliminates the explicit existence check by creating a fresh value from default_factory the first time a key is touched.
queue = [] queue.insert(0, item) # O(n) -- shifts every existing element from collections import deque queue = deque() queue.appendleft(item) # O(1) list.insert(0, x) and list.pop(0) are O(n) because every remaining element must shift; deque supports both ends in O(1) and should be used whenever you add/remove from the front.
Why does accessing a missing key on a Counter not raise KeyError, unlike a plain dict?
Counter overrides __missing__ to return 0 for any key that hasn't been counted yet, rather than raising KeyError -- this makes incrementing counts (counts[key] += 1) work without a prior existence check.
What performance problem does deque solve that a plain list has?
list.insert(0, x) and list.pop(0) are O(n) because every remaining element has to shift over; deque supports appendleft/append and popleft/pop in O(1) at both ends, making it the right structure for queues and sliding windows.
Since Python 3.7 guarantees dict insertion order, what does OrderedDict still offer that a plain dict does not?
move_to_end(key) to reposition an existing key, and an __eq__ that is order-sensitive -- two OrderedDicts with the same key-value pairs in different orders compare unequal, while two plain dicts compare equal regardless of order.
How does a ChainMap resolve a key that exists in more than one of its underlying mappings?
It returns the value from the first mapping (in the order passed to the constructor) that contains that key; writes through the ChainMap only ever go to the first mapping, leaving the others untouched.
Why might subclassing dict directly behave surprisingly, and what does UserDict fix?
Some of dict's C-implemented methods can bypass Python-level method overrides for performance (a CPython implementation detail), so an overridden __setitem__ isn't always called by every operation; UserDict stores data in a plain self.data attribute and implements everything in ordinary Python methods, so overrides are reliably honored.
What is the practical difference between a namedtuple and a frozen dataclass for representing a simple immutable record?
A namedtuple is a true tuple subclass -- it supports indexing and unpacking directly and has no per-instance __dict__ -- while a frozen dataclass is a richer, more explicit class with type annotations, default values, and generated methods, at the cost of not being tuple-compatible.
Write a function that takes a list of log-level strings and returns the two most common levels using Counter.
from collections import Counter def top_levels(levels: list[str], n: int = 2) -> list[tuple[str, int]]: return Counter(levels).most_common(n) print(top_levels(['INFO', 'ERROR', 'INFO', 'DEBUG', 'ERROR', 'INFO']))
Counter(levels) tallies occurrences of each string, and most_common(n) returns the n highest-count (item, count) pairs sorted descending -- here [('INFO', 3), ('ERROR', 2)].
What does this print, and why?
deque([2, 3, 4], maxlen=3) -- because maxlen bounds the deque at 3 items, appending 4 on the right automatically discards the oldest item (1) from the left.
Write a function that groups a list of words by their first letter using defaultdict(list).
from collections import defaultdict def group_by_first_letter(words: list[str]) -> dict[str, list[str]]: groups = defaultdict(list) for w in words: groups[w[0]].append(w) return dict(groups) print(group_by_first_letter(['apple', 'ant', 'banana', 'bee'])) defaultdict(list) creates an empty list automatically the first time a new first-letter key is seen, so each word can be appended without checking whether its key already exists -- result: {'a': ['apple', 'ant'], 'b': ['banana', 'bee']}.
What does this print?
True False -- namedtuple generates __eq__ that compares field values (like a regular tuple), so p1 == p2 is True even though they are two distinct tuple objects, so p1 is p2 is False.
from collections import deque
q = deque([1, 2, 3], maxlen=3)
q.append(4)
print(q)from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1 == p2, p1 is p2)