01 Β· DSA β€” File 09

Python Interview Cheatsheet

You already write Python daily β€” this page is about writing it fast, correct, and idiomatic under a timer. Every section is: the tool, the two lines you'll actually type, and the trap. Skim it the night before; the goal is zero syntax hesitation during the interview.

01collections β€” the four you'll actually use

Counter β€” frequency maps for free

from collections import Counter

c = Counter("mississippi")        # {'i': 4, 's': 4, 'p': 2, 'm': 1}
c.most_common(2)                  # [('i', 4), ('s', 4)]
c["z"]                            # 0 β€” missing keys are 0, no KeyError
Counter(a) == Counter(b)          # anagram check, one line
c1 - c2                           # multiset difference (drops ≀0 counts)

Use for: anagrams, top-k frequent, character budgets (ransom note: not (Counter(note) - Counter(magazine))).

defaultdict β€” kill the "if key not in d" dance

from collections import defaultdict

graph = defaultdict(list)         # adjacency lists
graph[u].append(v)                # no KeyError, ever

groups = defaultdict(list)        # grouping by canonical key
groups[key].append(item)

counts = defaultdict(int)         # manual counting (or just use Counter)
Trap: reading d[missing] creates the key (it calls the factory). Use in checks or .get() when probing without inserting.

deque β€” the O(1)-both-ends workhorse

from collections import deque

q = deque([start])
q.append(x)                       # right push
q.popleft()                       # left pop β€” THE reason it exists; list.pop(0) is O(n)
q.appendleft(x); q.pop()          # it's double-ended
dq = deque(maxlen=k)              # auto-evicting window buffer

Use for: BFS (always), sliding-window max (monotonic deque), any queue.

OrderedDict β€” mostly for LRU

Modern dicts preserve insertion order already; OrderedDict earns its keep via move_to_end(key) + popitem(last=False) β€” which is a complete LRU cache:

from collections import OrderedDict

class LRUCache:
    def __init__(self, capacity):
        self.cap, self.d = capacity, OrderedDict()
    def get(self, key):
        if key not in self.d: return -1
        self.d.move_to_end(key)                  # mark recently used
        return self.d[key]
    def put(self, key, value):
        self.d[key] = value
        self.d.move_to_end(key)
        if len(self.d) > self.cap:
            self.d.popitem(last=False)           # evict least-recent
If asked to build LRU "from scratch," that's hashmap + doubly linked list β€” file 03. Offer both; ask which they want.

02heapq β€” min-heap on a plain list

import heapq

heapq.heapify(nums)               # O(n), in place; nums[0] is the min
heapq.heappush(h, x)              # O(log n)
x = heapq.heappop(h)              # O(log n)
h[0]                              # peek, O(1)
heapq.heappushpop(h, x)           # push-then-pop, one sift β€” top-K loops
heapq.nlargest(k, data, key=f)    # quick top-K without manual heap code

The three rules (details in file 06):

Min-heap only

Python has no max-heap β€” negate values on push and pop to simulate one.

Tuples compare element-wise

Use (priority, tiebreaker, item) β€” a monotonic counter as tiebreaker stops Python from comparing unorderable payloads on ties.

No decrease-key

Push duplicates, skip stale entries on pop (the lazy-deletion idiom Dijkstra relies on).

maxheap = [-x for x in nums]; heapq.heapify(maxheap)
biggest = -heapq.heappop(maxheap)

03Sorting: sorted, .sort(), and key=

sorted(xs)                        # new list; sorted(s) works on any iterable
xs.sort()                         # in place, lists only; both are stable Timsort

xs.sort(key=lambda p: p[1])                   # by second field
xs.sort(key=lambda p: (-p[1], p[0]))          # DESC count, then ASC name β€”
                                              # the negate-inside-a-tuple trick
words.sort(key=len)                           # any callable works
intervals.sort()                              # tuples/lists sort lexicographically
                                              # β€” often exactly what you want
  • key= runs once per element (decorate-sort-undecorate) β€” cheap even for expensive keys.
  • Stability means equal keys keep order β‡’ multi-pass multi-key sorts work (file 08 Β§4).
  • For non-key-expressible comparators (rare β€” e.g., Largest Number's "a+b vs b+a"): functools.cmp_to_key(cmp).
  • min(xs, key=f) / max(xs, key=f) accept the same key β€” grab "best by criterion" without sorting.

bisect β€” binary search you don't have to write

import bisect
i = bisect.bisect_left(a, x)      # first index with a[i] >= x
j = bisect.bisect_right(a, x)     # first index with a[i] >  x
count = j - i                     # occurrences of x in sorted a
bisect.insort(a, x)               # insert keeping sorted (O(n) β€” the shift!)
Whenever you're tempted to hand-write binary search on a sorted list, say "I'll use bisect_left, which is the find-first-β‰₯ boundary" β€” correct and faster to write. (When the search is over an answer space, not a list, hand-write the template β€” file 08 Β§7.)

04Comprehensions & iteration idioms

squares = [x * x for x in xs if x > 0]            # list comp: transform+filter
seen = {canonical(x) for x in xs}                 # set comp
index_of = {v: i for i, v in enumerate(xs)}       # dict comp β€” Two Sum prep
total = sum(x * x for x in xs)                    # generator: no intermediate list

grid = [[0] * cols for _ in range(rows)]          # 2-D init β€” THE right way
                                                  # (see Β§06 for the wrong way)

for i, x in enumerate(xs): ...                    # index + value; never range(len(...)) alone
for a, b in zip(xs, ys): ...                      # parallel iteration
for x in reversed(xs): ...                        # no copy, unlike xs[::-1]
if any(p(x) for x in xs): ...                     # short-circuits
if all(p(x) for x in xs): ...
a, b = b, a                                       # swap, no temp
first, *rest = xs                                 # unpacking

Slicing β€” each makes a copy, O(k):

xs[2:5]   xs[:3]   xs[3:]   xs[::-1]   xs[::2]

05Strings β€” the toolbelt

s.split()                 # on any whitespace, drops empties β€” usually what you want
s.split(",")              # on a literal; keeps empties
" ".join(words)           # inverse; join is THE O(n) way to build strings
s.strip() / lstrip / rstrip
s.lower(); s.upper()
s.startswith(p); s.endswith(p)
s.find(t)                 # index or -1        s.index(t)  # index or ValueError
s.replace(a, b)           # all occurrences, new string
ch.isdigit(); ch.isalpha(); ch.isalnum()
ord('a'), chr(97)         # count-array indexing: ord(c) - ord('a')
f"{name}: {val:.2f}"      # f-strings for any output formatting
int("42"), str(42)        # and int("ff", 16) for base parsing
Remember: strings are immutable β€” build with a list + join, never += in a loop (files 01/02).

06The gotchas that eat interview minutes

Mutable default arguments

The bug

def f(x, acc=[]):   # [] created ONCE,
    acc.append(x)   # shared across ALL calls
    return acc
f(1); f(2)          # β†’ [1, 2]  !!

The fix

def f(x, acc=None):
    if acc is None:
        acc = []
    ...

Corollary: dict/list defaults in recursive helpers (memo={}) technically "work" for single-use functions but leak state between top-level calls β€” in interviews, pass memo explicitly or use @lru_cache and say why.

Aliasing vs copying

b = a                    # SAME list, two names
b = a[:]                 # shallow copy β€” new list, same element objects
import copy; copy.deepcopy(a)     # full clone (rarely needed; know it exists)

row = [0] * 3
grid = [row] * 4         # ← FOUR REFERENCES TO ONE ROW
grid[0][0] = 9           # every row now starts with 9  !!
grid = [[0] * 3 for _ in range(4)]    # the fix β€” fresh row per iteration
The [[0]*c]*r bug is the single most common Python interview self-own. The backtracking cousin: results.append(path) vs results.append(path[:]) (file 07).

Small but real

5 // 2 == 2;  -5 // 2 == -3      # floor division truncates DOWN (not toward 0)
5 % 3 == 2;  -5 % 3 == 1         # % result has the divisor's sign
round(2.5) == 2                  # banker's rounding β€” round-half-to-EVEN
x == y  vs  x is y               # value equality vs identity; use `is` only for None
                                 # (and cycle-detection node comparisons: slow is fast)
sys.setrecursionlimit(10**6)     # default ~1000; deep DFS on lists/trees can hit it β€”
                                 # mention it, or go iterative
float('inf'), float('-inf')      # sentinels for min/max tracking
d.get(k, default)                # probe without KeyError or insertion
for k, v in d.items(): ...       # don't iterate keys then re-index

Late binding in loops (occasionally bites in closures)

fns = [lambda: i for i in range(3)]
[f() for f in fns]               # [2, 2, 2] β€” closures capture the VARIABLE
fns = [lambda i=i: i for i in range(3)]   # fix: default-arg snapshot β†’ [0, 1, 2]

07Gotcha flashcards β€” drill until instant

What does this print / what's wrong?

Read the snippet, answer out loud, then reveal.

def f(x, acc=[]):
    acc.append(x)
    return acc
print(f(1)); print(f(2))
[1] then [1, 2]. The default [] is evaluated once at def time and shared across calls. Fix: acc=None + if acc is None: acc = [].
g = [[0] * 3] * 3
g[0][0] = 9
print(g)
[[9,0,0],[9,0,0],[9,0,0]]. Outer * copies the reference to one row three times. Fix: [[0]*3 for _ in range(3)] β€” fresh row per iteration.
print(-7 // 2)
print(-7 % 2)
-4 and 1. Floor division rounds down (not toward zero), and % takes the divisor's sign. Matters for midpoints and modular indexing.
fns = [lambda: i for i in range(3)]
print([f() for f in fns])
[2, 2, 2]. Closures capture the variable, not its value at creation β€” all three see the final i. Fix: default-arg snapshot: lambda i=i: i β†’ [0, 1, 2].
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b, a is b)
True False. == compares values; is compares identity (same object). Use is only for None checks and node-identity comparisons (e.g. cycle detection: slow is fast).
d = {}
d["b"] = 1; d["a"] = 2
print(list(d))
['b', 'a']. Dicts preserve insertion order (guaranteed since 3.7) β€” they are never auto-sorted. Want sorted keys? sorted(d). Want LRU-style reordering? OrderedDict.move_to_end.
a = [[1], [2]]
b = a[:]
b[0].append(9)
print(a)
[[1, 9], [2]]. a[:] is a shallow copy β€” new outer list, same inner element objects. Fix when elements are mutable: copy.deepcopy(a) (or copy each row: [row[:] for row in a]).
s = ""
for ch in chunks:
    s += ch
Accidental O(nΒ²). Strings are immutable, so every += copies the whole accumulated string. Fix: append parts to a list, then one "".join(parts) β€” O(n) total.

08functools and friends

from functools import lru_cache, cache

@cache                            # 3.9+; = lru_cache(maxsize=None)
def solve(i, remaining): ...      # instant memoization β€” args must be hashable
                                  # (tuples yes, lists no)

solve.cache_clear()               # between test cases if state matters

import itertools
itertools.permutations(xs, r)     # when the problem ALLOWS library enumeration
itertools.combinations(xs, r)     # (interviewer may want manual backtracking β€” ASK)
itertools.product(range(3), repeat=2)   # grid coordinates, cartesian choices
itertools.accumulate(xs)          # running prefix sums, one line
itertools.groupby(sorted_xs, key=f)     # run-length-style grouping (must be pre-sorted!)

import math
math.inf, math.ceil(a / b), math.gcd(a, b), math.isqrt(n)
(a + b - 1) // b                  # ceil-division without floats β€” worth knowing
On @cache in interviews: using it for DP is legitimate and fast β€” narrate "this memoizes on the argument tuple; state space is O(nΒ·m) so time is O(nΒ·m)". If they want the mechanics, hand-roll the dict.

09Writing clean interview code, fast

Structure to type without thinking:

def solve(nums: list[int], target: int) -> int:
    # 1. guard the trivial cases (empty, single) β€” cheap correctness points
    if not nums:
        return 0
    # 2. name your state clearly β€” left/right, lo/hi, seen, best, window
    best = 0
    seen: set[int] = set()
    # 3. the core loop, with the invariant you SAID OUT LOUD as a comment
    for right, x in enumerate(nums):
        ...
    return best

Habits that read as senior:

  • Small helper functions (def neighbors(r, c):, def feasible(k):) β€” they shrink the core loop to the algorithm's shape and give you clean units to test verbally.
  • Descriptive-but-short names: lo/hi, seen, freq, best, cur are idiomatic; x1, temp2, data are not. Single letters fine for indices only.
  • Early returns over nesting. if not valid: continue beats a 4-deep if-pyramid.
  • Type hints on the signature only β€” one line of professionalism, zero time cost. Skip them inside the body.
  • Walk one small example through your code, out loud, before declaring done. Pick an input of size 2–3 plus one edge case (empty, duplicates, all-same). This catches ~80% of bugs and is itself assessed.
  • Don't golf. A comprehension is idiomatic; three nested comprehensions with a walrus is a code smell under interview conditions.
The 25-minute routine (self-test Q10): restate + constraints β†’ brute force & target complexity out loud β†’ pick pattern/structure β†’ code with invariant comments β†’ trace a small example + edge case, then state time/space.

The "which structure" reflex table (details in files 02–06):

NeedReach for
membership / dedupeset
key β†’ value, counting, groupingdict / Counter / defaultdict(list)
queue / BFSdeque
stacklist
repeated min/maxheapq
sorted-order ops on static datasorted() + bisect

10The big ops-cost table

Every cost you should be able to recite

StructureOperationCostNote
lista[i], a[i] = xO(1)address arithmetic
lista.append(x), a.pop()O(1) amortizedtail has spare capacity β€” this is your stack
lista.insert(0, x), a.pop(0)O(n)shifts everything β€” use deque instead
lista[i:j] sliceO(jβˆ’i)copies into a new list β€” includes a[:] and a[::-1]
listx in aO(n)linear scan β€” reach for set
lista.sort() / sorted(a)O(n log n)stable Timsort; key= computed once per element
dictd[k], d[k] = v, del d[k]O(1) averagehashing; keys must be hashable
dictk in d, d.get(k, default)O(1) average.get probes without KeyError or insertion
dictfor k, v in d.items()O(n)insertion order preserved (3.7+)
setadd, remove, x in sO(1) averagemembership / dedupe workhorse
dequeappend, appendleft, pop, popleftO(1)the reason it exists β€” BFS queue
dequedq[i] middle indexingO(n)it's a linked block structure, not an array
heapqheapify(nums)O(n)in place; nums[0] becomes the min
heapqheappush, heappop, heappushpopO(log n)min-heap only β€” negate for max
heapqh[0] peekO(1)never pop just to look
heapqnlargest(k, data, key=f)O(n log k)quick top-K without manual heap code
bisectbisect_left, bisect_rightO(log n)find-first-β‰₯ / find-first-> boundaries
bisectinsort(a, x)O(n)the search is log n but the insert shifts
strs += ch in a loopO(nΒ²) totalimmutable β€” every += copies the accumulated string
str"".join(parts)O(n)THE way to build strings
strs[i:j], s[::-1]O(k)slices copy, same as lists
strt in s, s.find(t)O(nΒ·m) worstfast in practice; fine to use in interviews
CounterCounter(xs)O(n)missing keys read as 0, no KeyError
Countermost_common(k)O(n log k)heap under the hood
OrderedDictmove_to_end, popitem(last=False)O(1)the two methods that make it an LRU cache

11Common interview questions (brief answers)

Q1. Why is [[0]*c]*r wrong?

Outer * copies the reference to one row list r times; writes through any row alias hit all. Fix: comprehension with a fresh [0]*c per row.

Q2. What does key= in sort actually do?

Computes a sort key once per element, sorts by keys (decorate-sort-undecorate). Descending on one field of several: negate that field inside the key tuple.

Q3. bisect_left vs bisect_right in one line each?

Left: first index with a[i] >= x. Right: first with a[i] > x. Difference = count of x.

Q4. Max-heap in Python?

Negate on push and pop, or negate the priority in a (-priority, item) tuple. heapq is min-only.

Q5. Why does def f(x, acc=[]) misbehave?

Default evaluated once at def-time; the same list persists across calls. Use None sentinel.

Q6. Shallow vs deep copy?

Shallow (a[:], list(a), .copy()): new container, shared elements. Deep: recursive clone. Matters when elements are mutable (lists of lists).

Q7. Why prefer deque over list for BFS?

popleft() O(1) vs pop(0) O(n); with n dequeues that's O(n) vs O(nΒ²).

Q8. Is @lru_cache acceptable for DP in interviews?

Usually yes with narration of state space and complexity; be ready to hand-roll the memo dict if asked. Args must be hashable.

Q9. -7 // 2 and -7 % 2?

-4 and 1 β€” floor division rounds down, % matches the divisor's sign. Matters for midpoints and modular indexing.

Q10. Fastest anagram / top-k-frequent / adjacency setup?

Counter(s) == Counter(t); Counter(xs).most_common(k) (or heap for O(n log k)); defaultdict(list) + edge loop.

12Self-test

  1. From memory: the one-liner each for (a) anagram check, (b) value→index map, (c) rows×cols zero grid, (d) count of x in a sorted list.
  2. What's wrong with def dfs(node, path=[])? Show the fixed signature.
  3. Sort a list of (name, count) by count descending, then name ascending β€” the exact key=.
  4. Why is key= called O(n) times rather than O(n log n)? What idiom is this called?
  5. Build a max-heap of (priority, task_dict) items where priorities can tie. What breaks naively, and what's the fix?
  6. Predict: a = [[0]*2]*3; a[1][0] = 5; print(a). Then write the correct initializer.
  7. deque(maxlen=3) β€” what happens on the 4th append, and name one problem where that's useful.
  8. Write the LRU cache with OrderedDict from memory (both methods).
  9. Predict: [-3 // 2, -3 % 2, round(0.5), round(1.5)].
  10. You have 25 minutes and a medium problem. List your five-step routine from reading the prompt to "done."
Spot-checks
  1. Counter(s)==Counter(t); {v:i for i,v in enumerate(xs)}; [[0]*cols for _ in range(rows)]; bisect_right(a,x)-bisect_left(a,x).
  2. Shared default list across calls; path=None + if path is None: path=[].
  3. key=lambda t: (-t[1], t[0]).
  4. Keys precomputed once, then compared during merges β€” decorate-sort-undecorate.
  5. Ties force comparison of dicts β†’ TypeError; add a monotonic counter: (-priority, count, task).
  6. [[5,0],[5,0],[5,0]]; [[0]*2 for _ in range(3)].
  7. Oldest element auto-evicted from the left; sliding-window / last-k-items problems.
  8. See Β§01 β€” get: missβ†’βˆ’1, hitβ†’move_to_end+return; put: set, move_to_end, evict popitem(last=False) when over capacity.
  9. [-2, 1, 0, 2].
  10. Restate + constraints β†’ brute force & target complexity out loud β†’ pick pattern/structure β†’ code with invariant comments β†’ trace a small example + edge case, then state time/space.