Skip to content

The collections Module Interview Questions & Answers

16 questions Updated 2026-06-18 Share:

Python interview questions on the collections module — Counter, defaultdict, deque, OrderedDict, ChainMap, and how they relate to namedtuple.

Read the in-depth guidePython collections Module Explained — Counter, defaultdict, and deque(opens in new tab)
16 of 16

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 ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel