Preamble

Interview Python rarely needs exotic libraries: itertools and collections cover most combinatorial, frequency, and sliding-window problems with clear intent and predictable complexity. This post is a practical map—what to import, what each tool is good for, and the footguns that lose you the round.


itertools: combinations without materializing lists

combinations(iterable, r) chooses unordered subsets of size r. permutations(iterable, r) cares about order; omit r to permute the full length. Saying the difference out loud prevents subtle bugs in constraint and backtracking setups.

product(*iterables, repeat=n) is the Cartesian product—nested loops as one iterator. combinations_with_replacement allows repeated picks when order does not matter but elements can repeat (e.g. coin-change style enumeration).

from itertools import combinations, permutations, product

list(combinations("ABC", 2))   # [('A','B'), ('A','C'), ('B','C')]
list(permutations("AB", 2))    # [('A','B'), ('B','A')]
list(product([0, 1], repeat=2))  # (0,0), (0,1), (1,0), (1,1)

itertools: chaining and flattening

chain(*iterables) walks many iterables one after another without building a concatenated list. chain.from_iterable flattens one level of nesting—common after grouping or splitting.

zip_longest(*iterables, fillvalue=...) pairs streams of unequal length; plain zip stops at the shortest and can hide bugs when lengths differ on purpose.

from itertools import chain, zip_longest

list(chain([1, 2], [3], [4, 5]))           # [1, 2, 3, 4, 5]
list(chain.from_iterable([[1, 2], [3]]))   # [1, 2, 3]
list(zip_longest("ab", "xyz", fillvalue="-"))  # [('a','x'), ('b','y'), ('-','z')]

itertools: windows, prefixes, and filtering iterators

pairwise(iterable) (Python 3.10+) yields adjacent pairs—clean for “compare neighbor” scans. Before 3.10, zip(s, s[1:]) on a sequence or a manual iterator step.

accumulate(iterable[, func]) is running sum by default; pass max, min, or a custom binary function for running aggregates.

islice(iterable, start, stop, step) slices without copying—useful on infinite iterators (count, cycle) or to cap work.

takewhile / dropwhile split or skip prefixes by predicate; they do not re-evaluate the middle—know where the cut is.

groupby(iterable, key=...) groups consecutive runs only. Sort first (or otherwise arrange adjacency) if you need global grouping—it is not SQL GROUP BY.

from itertools import accumulate, groupby

list(accumulate([1, 2, 3, 4]))           # [1, 3, 6, 10]
runs = [(k, list(g)) for k, g in groupby("aaabbc")]
# [('a', ['a','a','a']), ('b', ['b','b']), ('c', ['c'])]

collections.Counter: frequencies in one object

Counter(iterable) counts hashable items; .most_common(k) returns the top k pairs—interview shorthand for “majority,” “top k,” or validation of character counts.

Counter supports addition and subtraction of counts (multiset-style), and intersection via & / union via | when you model “minimum/maximum count per key” puzzles.

from collections import Counter

Counter("abracadabra").most_common(2)  # [('a', 5), ('b', 2)]
Counter("aabc") - Counter("ab")        # Counter({'a': 1, 'c': 1})

collections.deque: O(1) ends and BFS

deque gives O(1) append, appendleft, pop, popleft. It is the default structure for BFS (queue) and for sliding windows of fixed size when you only need ends—not random access in the middle.

For “window of k elements,” maintain indices or store pairs (index, value) if you need to drop stale entries from the left.

from collections import deque

q = deque([1, 2, 3])
q.append(4)
q.popleft()   # 1

defaultdict: graphs, grouping, and “first touch” maps

defaultdict(factory) supplies a default for missing keys so you avoid if key not in d boilerplate. Typical factories: list (adjacency lists, grouping), int (counting with +=), set.

Interview tip: defaultdict(list) for graph[u].append(v) and for buckets[key].append(item) reads faster than manual setdefault.

from collections import defaultdict

dd = defaultdict(list)
dd["a"].append(1)   # no KeyError

If you need exactly one default for all missing keys (e.g. a shared mutable default is wrong), use dict.get(key, default) or initialize explicitly—defaultdict is for per-key fresh defaults from the factory.


collections.OrderedDict and deque vs list

OrderedDict remembers insertion order (since 3.7, plain dict does too). It still matters for move_to_end and a few LRU-style patterns; many interviews are fine with dict + explicit ordering logic.

For FIFO eviction, deque with maxlen gives a bounded window without manual index juggling.


When to drop to indexes

Random access, heavy pruning in backtracking, or in-place array algorithms still want classic loops and indexing—see N-Queens and DFS style posts. itertools shines for forward-only generation; Counter and defaultdict shine when the problem is aggregation or sparse structure, not pointer chasing on a tiny fixed grid.


Conclusion

itertools names the combinatorial shape of the search; collections names the shape of the data you accumulate. Together they reduce custom bookkeeping and free mental bandwidth for the actual invariant. Poetry for Dependency Management and Packaging consolidates dependencies with Poetry—reproducible graphs when the interview turns into a real service.