Counter is a dict subclass for counting hashable items. You pass it
any iterable and it tallies occurrences into a {element: count} mapping, with
handy extras like most_common(). Missing keys return 0 instead of raising.
from collections import Counter
c = Counter("mississippi")
# Counter({'i': 4, 's': 4, 'p': 2, 'm': 1})
c.most_common(2) # [('i', 4), ('s', 4)] — top N by count
c["z"] # 0 — missing key, no KeyError
c.update("pp") # add more counts -> p becomes 4
Counter([1, 1, 2]) + Counter([1, 3]) # Counter({1: 3, 2: 1, 3: 1})
It replaces the manual d[x] = d.get(x, 0) + 1 pattern and supports arithmetic
between counters. Reach for Counter whenever the task is "how many of each?" —
word frequencies, tallies, or finding the most common elements.
defaultdict(factory) is a dict that auto-creates a missing key's value
by calling the zero-argument factory (like list, int, or set) the first
time you access it — so you can append or increment without initializing.
from collections import defaultdict
groups = defaultdict(list)
groups["fruits"].append("apple") # key auto-created as [] then appended
# {'fruits': ['apple']}
counts = defaultdict(int)
for ch in "aab":
counts[ch] += 1 # missing keys start at 0
# {'a': 2, 'b': 1}
The difference from setdefault: setdefault evaluates its default
on every call even when the key exists (wasteful), whereas defaultdict
only calls the factory on a miss. Use defaultdict for grouping and
counting loops; use plain setdefault for a one-off default insertion.
A deque ("double-ended queue") gives O(1) appends and pops at both
ends, whereas a list is O(n) for operations at the front (every element
shifts). It also supports a maxlen for a fixed-size sliding window.
from collections import deque
d = deque([1, 2, 3])
d.appendleft(0) # O(1) — list.insert(0, x) would be O(n)
d.append(4) # O(1)
d.popleft() # O(1) — efficient FIFO queue
window = deque(maxlen=3)
for x in [1, 2, 3, 4]:
window.append(x) # auto-drops from the left
# deque([2, 3, 4], maxlen=3)
Use a deque for queues, BFS, and sliding windows; with maxlen it
auto-discards the oldest item when full (great for "last N" buffers). Stick with
a list when you only push/pop at the end or need fast random indexing.
Mostly not for ordering itself — since Python 3.7 a plain dict already
preserves insertion order, so OrderedDict is no longer needed just to remember
order. But it still has a couple of distinct features a regular dict lacks.
from collections import OrderedDict
# 1) order-sensitive equality
OrderedDict(a=1, b=2) == OrderedDict(b=2, a=1) # False
dict(a=1, b=2) == dict(b=2, a=1) # True (order ignored)
# 2) move_to_end and a popitem(last=...) toggle
od = OrderedDict(a=1, b=2, c=3)
od.move_to_end("a") # OrderedDict([('b',2),('c',3),('a',1)])
od.popitem(last=False) # pop from the FRONT — FIFO
So use OrderedDict when you need order-sensitive ==, move_to_end,
or popitem(last=False) — for example, building an LRU cache. For plain
"keep the order I inserted," a regular dict is now enough.
ChainMap groups multiple dicts into a single, layered view without
copying them. Lookups search the underlying mappings in order and return the
first match, so earlier maps shadow later ones.
from collections import ChainMap
defaults = {"color": "red", "size": "M"}
overrides = {"color": "blue"}
settings = ChainMap(overrides, defaults)
settings["color"] # 'blue' — first map wins
settings["size"] # 'M' — falls through to defaults
settings["size"] = "L" # writes go to the FIRST map (overrides) only
It's perfect for layered configuration (CLI args over env vars over defaults) and scope-like lookups, because it stays live — changing a source dict shows up immediately. Note that writes and deletes only affect the first mapping. Use it to merge config layers without flattening them into one dict.
collections.namedtuple is a factory that creates an immutable tuple
subclass with named fields — lightweight, hashable records that read like
objects but behave like tuples (indexing, unpacking, comparison).
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
p.x, p[0] # 3, 3 — named and positional access
p._asdict() # {'x': 3, 'y': 4}
p._replace(x=9) # Point(x=9, y=4) — returns a NEW tuple (immutable)
It rounds out the module's record-like tools alongside Counter,
defaultdict, and deque. For richer needs — type hints, defaults, methods —
typing.NamedTuple or a @dataclass is the modern choice. Use namedtuple for
simple, immutable, self-documenting records.
Use Counter.most_common(n), which returns a list of (item, count)
tuples sorted by count descending. With no argument it returns all items
ordered by frequency. It's the idiomatic one-liner for "top N" problems.
from collections import Counter
words = "a b a c a b".split()
Counter(words).most_common(2) # [('a', 3), ('b', 2)]
Rule of thumb: reach for Counter(iterable).most_common(n) instead of
hand-rolling a frequency dict plus a sort.
Counters support +, -, & (min/intersection), and | (max/union),
treating counts like multiset multiplicities. Addition combines counts;
subtraction drops counts at or below zero. This makes combining or diffing
frequency tables trivial.
from collections import Counter
a, b = Counter("aab"), Counter("abc")
a + b # Counter({'a': 3, 'b': 2, 'c': 1})
a - b # Counter({'a': 1}) negatives dropped
a & b # Counter({'a': 1, 'b': 1}) per-key min
Rule of thumb: use Counter arithmetic to merge or compare counts instead of looping over keys manually.
Yes — unlike a true multiset, a Counter lets you store and update
zero or negative values directly (e.g. c["x"] -= 5). Those keys stay in the
dict. Only the arithmetic operators (+, -) and most_common/
+Counter() strip non-positive counts; elements() skips them too.
from collections import Counter
c = Counter(a=2, b=0, c=-1)
list(c.elements()) # ['a', 'a'] -> ignores b and c
+c # Counter({'a': 2}) unary + drops <=0
Rule of thumb: direct assignment keeps any int; use +c or arithmetic when
you want only positive counts.
The default_factory is any zero-arg callable producing the default value:
int for counters (0), list for grouping ([]), set for
unique grouping, and even dict for nested maps. Accessing a missing key
calls the factory and stores the result.
from collections import defaultdict
groups = defaultdict(list)
for name, dept in people:
groups[dept].append(name) # no KeyError, list auto-created
counts = defaultdict(int)
for ch in text: counts[ch] += 1
Rule of thumb: pick the factory by the value type you accumulate — list/
set to group, int to count, dict to nest.
Reading a missing key creates and inserts it, which can silently grow
the dict or change membership checks. A mere d[k] (or even k in d after a
bracket access) is no longer side-effect-free. Use .get() or k in d when
you only want to peek.
from collections import defaultdict
d = defaultdict(list)
_ = d["missing"] # inserts 'missing': [] !
print(dict(d)) # {'missing': []}
Rule of thumb: read with d.get(k) when you don't intend to create the key;
reserve bracket access for when auto-creation is what you want.
maxlen caps the deque's size: appending to a full deque discards the
item at the opposite end. This gives you a fixed-size sliding window or a
"last N items" buffer for free, with O(1) appends.
from collections import deque
last3 = deque(maxlen=3)
for x in range(5):
last3.append(x)
last3 # deque([2, 3, 4], maxlen=3)
Rule of thumb: use a maxlen deque for rolling logs, moving windows, or
"keep only the most recent N" without manual trimming.
rotate(n) shifts elements right by n (left if negative) in O(k).
deque gives O(1) appends and pops at both ends (append/appendleft/
pop/popleft), versus a list's O(n) insert(0)/pop(0). Indexing the
middle, though, is O(n) for a deque.
from collections import deque
d = deque([1, 2, 3, 4])
d.rotate(1) # deque([4, 1, 2, 3])
d.rotate(-2) # deque([2, 3, 4, 1])
Rule of thumb: use deque for queue/stack workloads with end operations; use a list when you need fast random indexing.
A ChainMap searches its mappings left to right for reads, but all writes/deletes affect only the first mapping. This is perfect for layered configs (CLI > env > defaults) where you want a writable top layer over read-only fallbacks.
from collections import ChainMap
defaults = {"color": "red", "size": "M"}
cfg = ChainMap({}, defaults)
cfg["color"] = "blue" # writes to the first (empty) map
cfg["color"], cfg["size"] # ('blue', 'M')
defaults # unchanged
Rule of thumb: use ChainMap to overlay scopes without copying/merging, with edits landing in the first layer only.
Yes — move_to_end(key, last=True) and popitem(last=...) give
explicit reordering, and OrderedDict equality is order-sensitive (a plain
dict's is not). That makes it handy for LRU-cache-style structures.
from collections import OrderedDict
od = OrderedDict(a=1, b=2, c=3)
od.move_to_end("a") # OrderedDict(b=2, c=3, a=1)
od.popitem(last=False) # ('b', 2) -> pops from the front
Rule of thumb: use a plain dict for ordering; reach for OrderedDict when you
need move_to_end, front-popping, or order-sensitive equality.
Subclassing built-in dict/list directly is leaky — internal methods don't
always call your overrides (e.g. dict.update may bypass __setitem__).
UserDict/UserList wrap the data in a .data attribute and route
everything through your overridden methods, so customization behaves
consistently.
from collections import UserDict
class UpperDict(UserDict):
def __setitem__(self, k, v):
super().__setitem__(k.upper(), v)
d = UpperDict(); d["a"] = 1
d.data # {'A': 1} -> override honored everywhere
Rule of thumb: subclass UserDict/UserList when you need reliable method
overrides; use plain subclasses only for trivial additions.
More Data Structures interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.