Lesson 11 · 10 min read · Python for placements

The standard library that wins coding rounds

collections, heapq, bisect, itertools, math and functools — the imports that turn thirty lines into five, plus fast input and the recursion limit.

Python's advantage in a timed coding round is not syntax. It is that a dozen things you would have to implement in Java or C++ are one import away, already correct and written in C.

These are the ones that actually come up.

collections

from collections import Counter, defaultdict, deque, namedtuple

Counter — counting anything.

c = Counter("placement")
print(c["e"])                  # 2
print(c.most_common(2))        # [('e', 2), ('p', 1)]
print(Counter("abc") == Counter("cba"))     # True — anagram check, one line

# subtracting counters gives what is missing
print(Counter("aab") - Counter("ab"))       # Counter({'a': 1})

Anagram questions, character-frequency questions and "most frequent element" questions all reduce to a Counter.

defaultdict — grouping without an if, covered in lesson 5.

deque — a double-ended queue with O(1) operations at both ends.

from collections import deque

q = deque([1, 2, 3])
q.append(4)         # right
q.appendleft(0)     # left,     O(1)   — a list would be O(n)
q.pop()             # right
q.popleft()         # left,     O(1)
q.rotate(1)         # rotate right
window = deque(maxlen=3)        # bounded — pushing a 4th drops the oldest

Any BFS is a deque. Any sliding-window-maximum question is a deque. Using a list and pop(0) is the difference between passing and timing out on a large input.

namedtuple — a tuple with names, when a dataclass is more than you need.

from collections import namedtuple
Point = namedtuple("Point", "x y")
p = Point(3, 4)
print(p.x, p[0])     # 3 3

heapq — a priority queue

Python has no PriorityQueue class you would want in a coding round; it has functions that treat a plain list as a min-heap.

import heapq

heap = [5, 1, 3]
heapq.heapify(heap)             # O(n), in place
heapq.heappush(heap, 2)         # O(log n)
print(heapq.heappop(heap))      # 1  — always the SMALLEST, O(log n)
print(heap[0])                  # peek without removing, O(1)

print(heapq.nsmallest(2, [5, 1, 3]))    # [1, 3]
print(heapq.nlargest(2, [5, 1, 3]))     # [5, 3]

It is a min-heap only. For a max-heap, negate:

values = [5, 1, 3]
max_heap = [-v for v in values]
heapq.heapify(max_heap)
print(-heapq.heappop(max_heap))         # 5

For "k largest / k smallest / k closest" questions, a heap of size k is O(n log k) and beats sorting the whole input. Push tuples to carry a payload — the first element is the priority:

heapq.heappush(heap, (distance, node))

bisect — binary search on a sorted list

import bisect

xs = [1, 3, 5, 7]
print(bisect.bisect_left(xs, 5))     # 2 — first index where 5 could go
print(bisect.bisect_right(xs, 5))    # 3 — after any existing 5s
bisect.insort(xs, 4)                 # insert keeping order
print(xs)                            # [1, 3, 4, 5, 7]

Two real uses: checking membership in a sorted list in O(log n), and maintaining a sorted list as values arrive. Note insort is O(n) — the search is logarithmic, the shift is not.

itertools

from itertools import (
    permutations, combinations, product, accumulate,
    groupby, chain, islice, count, cycle, repeat,
)

print(list(permutations([1, 2, 3], 2)))
# [(1,2), (1,3), (2,1), (2,3), (3,1), (3,2)]  — order matters

print(list(combinations([1, 2, 3], 2)))
# [(1,2), (1,3), (2,3)]                       — order does not

print(list(product([0, 1], repeat=2)))
# [(0,0), (0,1), (1,0), (1,1)]                — nested loops, flattened

print(list(accumulate([1, 2, 3, 4])))
# [1, 3, 6, 10]                               — running total / prefix sums

print(list(chain([1, 2], [3])))               # [1, 2, 3]
print(list(islice(count(10), 3)))             # [10, 11, 12]

accumulate gives prefix sums in one call, which is the backbone of range-sum questions. product(..., repeat=n) replaces n nested loops when n is a variable.

A word of warning: permutations of 10 items is 3.6 million tuples. These are lazy, so generating them is cheap; consuming all of them is not. If a question tempts you into permutations, check the input size first — that is usually the point of the question.

functools

from functools import lru_cache, cache, reduce

@cache                     # Python 3.9+; use @lru_cache(maxsize=None) below that
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

print(fib(100))            # instant — without the cache this is 2^100 calls

One decorator turns exponential recursion into linear. Every memoisation question — fibonacci, grid paths, coin change, stairs — is @cache plus the naive recursion. Be ready to also write the dictionary by hand, because some interviewers ask for it:

def fib(n, memo={}):        # dict default is safe here only because we never replace it
    if n < 2:
        return n
    if n not in memo:
        memo[n] = fib(n - 1, memo) + fib(n - 2, memo)
    return memo[n]

reduce folds a sequence into one value. Usually a loop or sum is clearer, but it appears in questions about functional style:

from functools import reduce
print(reduce(lambda a, b: a * b, [1, 2, 3, 4]))    # 24

math, and integer maths

import math

math.gcd(12, 18)          # 6
math.lcm(4, 6)            # 12, Python 3.9+
math.isqrt(17)            # 4 — exact integer square root, no float error
math.factorial(5)         # 120
math.comb(5, 2)           # 10
math.inf, -math.inf       # sentinels for "no minimum yet"
math.floor(-1.5)          # -2
math.ceil(1.2)            # 2

math.isqrt matters: int(n ** 0.5) goes through a float and can be off by one for large n, which silently breaks a primality test. And math.inf is the right initial value for a running-minimum, rather than picking a large number and hoping.

Reading input fast

In an online judge, input() called a hundred thousand times is slow enough to matter:

import sys

data = sys.stdin.read().split()          # everything at once
n = int(data[0])
values = list(map(int, data[1:n + 1]))

# or, line by line but still buffered
input = sys.stdin.readline               # rebind, then use input() as normal

And output: one print per line is slow, so build and write once.

sys.stdout.write("\n".join(map(str, results)) + "\n")

The recursion limit

Python's default recursion limit is 1000 frames. A recursive solution on an input of 10⁵ will raise RecursionError even though the logic is right:

import sys
sys.setrecursionlimit(10 ** 6)

Raise it when you must, and know the honest caveat: this raises Python's own guard, not the operating system's stack limit, so very deep recursion can still crash the process. Where you can, convert to an iterative solution with an explicit stack.

The eight-line cheat sheet

from collections import Counter, defaultdict, deque
import heapq, bisect, math, sys
from itertools import permutations, combinations, accumulate
from functools import cache

Type that from memory before you start a coding round, and most of what you need is already there.

What to take into an interview

  • Counter for frequencies and anagrams; defaultdict for grouping; deque for BFS and queues.
  • heapq is a min-heap on a plain list. Negate for a max-heap. Size-k heap beats a full sort.
  • bisect is binary search on a sorted list; insort keeps it sorted but is O(n).
  • itertools.accumulate gives prefix sums; product(repeat=n) replaces n nested loops.
  • @cache turns naive recursion into memoised recursion in one line.
  • math.isqrt avoids float error; math.inf is the right "no value yet".
  • sys.stdin.read() for large input; sys.setrecursionlimit for deep recursion.
Lesson 11 of 12 · all 12 are free to read, no account. Course contents