Python Idioms for Coding Interviews: The Cheat Sheet That Saves Minutes

May 29, 202611 min read
dsaalgorithmsinterview-preppython
Python Idioms for Coding Interviews: The Cheat Sheet That Saves Minutes
TL;DR
  • deque.popleft() runs in O(1); list.pop(0) shifts every element and is O(n), silently breaking BFS traversal complexity.
  • Counter subclasses dict, treats missing keys as zero, and supports arithmetic on frequency maps, but subtraction silently drops negative counts.
  • @cache memoizes any pure function in one decorator; arguments must be hashable and cache_clear() is needed between independent test cases.
  • bisect_left replaces a manual binary search on sorted arrays, returning the leftmost insertion point in O(log n).
  • defaultdict removes key-existence guards but silently inserts a default entry on read, which breaks membership checks.
  • [[0]*cols for _ in range(rows)] is correct 2D initialization; [[0]*cols]*rows creates aliased rows that mutate together.
  • Python's % always returns a non-negative result when the divisor is positive, unlike Java and C++, which matters for circular array index math.

Interview minutes are scarce. You get 45 of them, one problem, and a growing sense that you should have reviewed this stuff last night. Every line you don't have to think about buys back thinking time. The Python idioms below compress common patterns into one or two lines, but only if you know what they actually do and where they quietly bite you.

A fluent Python solution isn't just shorter. It's faster to write, easier to trace, and less likely to contain bugs. When you type out a manual frequency counter with nested if-else instead of Counter, you spend 30 seconds writing code you already know and open a bug opportunity. When you use collections.deque and call popleft() instead of list.pop(0), you also silently upgrade from O(n) to O(1). The interviewer may ask you to walk through complexity. Know which version you're running.


Python Idioms for Coding Interviews: Quick Reference

IdiomReplacesComplexity note
enumerate(arr)range(len(arr))Same O(n)
zip(a, b)manual index syncSame O(n)
Counter(arr)manual dict loopO(n) build
defaultdict(list)if key not in d guardsO(1) per access
deque.popleft()list.pop(0)O(1) vs O(n)
bisect.bisect_leftmanual binary searchO(log n)
any() / all()for-loop with flagShort-circuits
@cachemanual memo dictO(1) per repeated call
arr[::-1]reversed(arr) listO(n) new list
-1 % nedge case guardAlways positive in Python

Iteration Helpers

Stop Counting Manually

# before for i in range(len(arr)): print(i, arr[i]) # after for i, val in enumerate(arr): print(i, val)

enumerate accepts a start parameter. enumerate(arr, 1) starts indices at 1, useful for problems that want one-indexed output. Small thing, but it eliminates a whole class of off-by-one errors that always seem to appear at exactly the wrong moment.

Pair Two Sequences Without an Index

for a, b in zip(left, right): merged.append(min(a, b)) # transpose a matrix transposed = list(zip(*matrix))

zip(*matrix) is the one-liner for matrix transposition. It unpacks the matrix rows as arguments, and zip pairs the first element of each row, then the second, and so on. Cost is O(m * n). The asterisk is doing the heavy lifting. Stare at it until it makes sense, then commit it to muscle memory.

zip stops at the shorter sequence. If you need the longer one, use itertools.zip_longest. Most interview problems won't need it, but when they do, you'll be glad you knew.

Short-Circuit Search

has_negative = any(x < 0 for x in arr) is_sorted = all(arr[i] <= arr[i+1] for i in range(len(arr) - 1))

any stops at the first True. all stops at the first False. Pass a generator expression (no brackets) to avoid building the full list in memory before checking. The interviewer will not notice, but the computer will.


Data Structure Shortcuts

Frequency in One Line

from collections import Counter freq = Counter(arr) # {element: count} freq["missing"] # returns 0, not KeyError freq.most_common(3) # top 3 by count, O(n log k)

Counter subclasses dict. Accessing a missing key returns 0 instead of raising KeyError, which removes a full if-block from every frequency problem. Your future self will notice.

Counter arithmetic is underused in interviews. Counter(a) - Counter(b) gives you elements in a not covered by b, with zero and negative counts dropped automatically.

c1 = Counter("aab") c2 = Counter("ab") print(c1 - c2) # Counter({'a': 1})

One trap worth memorizing: subtraction silently discards negatives. Counter({'a': 1}) - Counter({'a': 3}) gives an empty Counter, not {'a': -2}. If you need the actual deficit, subtract the values manually. Counter is optimistic. It refuses to acknowledge debt.

Skip the Key Existence Check

from collections import defaultdict graph = defaultdict(list) graph[0].append(1) # no KeyError even if 0 was never set freq = defaultdict(int) for x in arr: freq[x] += 1

defaultdict(list) is cleaner than dict.setdefault(key, []) when building adjacency lists. Both cost O(1) per access.

Subtler trap: accessing a missing key in a defaultdict creates that key with the default value. Every time. Silently. If you're checking existence, use key in d or d.get(key). Calling d[key] when you only wanted to read inserts a new entry and gives you a populated dict that looks slightly wrong in ways that are very hard to debug under pressure.

BFS Without the O(n) Tax

from collections import deque q = deque() q.append(x) # right end, O(1) q.appendleft(x) # left end, O(1) q.pop() # right end, O(1) q.popleft() # left end, O(1)

list.pop(0) is O(n) because it shifts every remaining element. Use a list as a BFS queue and you've silently made the whole traversal O(V * E) instead of O(V + E). The code looks right. It gives the right answer. It just does it slowly, and complexity analysis is an explicit interview dimension.

deque also takes a maxlen argument. deque(maxlen=k) auto-discards old elements when it fills, useful for sliding-window problems where you want a fixed-size window without manual cleanup. One less thing to forget at 38 minutes into the clock.


Sorting and Binary Search

Custom Order Without a Comparator

pairs = sorted(arr, key=lambda x: x[1]) # multi-level sort: primary by start, secondary by end intervals = sorted(intervals, key=lambda x: (x[0], x[1])) # sort by length, then lexicographic words = sorted(words, key=lambda w: (len(w), w))

Tuples compare element-by-element, so a tuple key gives you multi-level sorting with no extra code. No cmp_to_key needed unless you have a genuinely custom three-way comparison, which comes up less often than you'd think.

For in-place sorting: arr.sort(key=...). sorted() returns a new list. Choose based on whether you want to preserve the original.

Binary Search Without Writing It

import bisect arr = [1, 3, 5, 7, 9] bisect.bisect_left(arr, 5) # 2: leftmost insertion point for 5 bisect.bisect_right(arr, 5) # 3: rightmost insertion point for 5 bisect.insort(arr, 6) # inserts 6 maintaining order

bisect_left(arr, target) returns the leftmost index where you could insert target to keep the list sorted. If target exists, it returns its index. If not, the insertion point. This is the function you reach for when you'd otherwise write a binary search from scratch and introduce a subtle off-by-one on the last iteration.

# count elements strictly less than target count_less = bisect.bisect_left(arr, target) # check if target exists idx = bisect.bisect_left(arr, target) exists = idx < len(arr) and arr[idx] == target

insort is O(n) because of the list shift after finding the position. Fine for small lists, not for maintaining a sorted structure at scale. Know what you're buying.


Memoization with One Decorator

from functools import cache # Python 3.9+ @cache def dp(i, j): if i == 0: return 0 return dp(i - 1, j) + dp(i, j - 1)

@cache converts any pure function into a memoized version with no boilerplate. It's equivalent to @lru_cache(maxsize=None) from earlier Python versions. One decorator, exponential call tree collapsed.

Two traps that catch people.

First: arguments must be hashable. You can't @cache a function that takes a list. Pass tuples instead, and convert at the call site.

# won't work: list is not hashable @cache def f(arr): ... # works @cache def f(arr: tuple): ...

Second: @cache stores every call forever for the lifetime of the function object. For a deeply recursive function on large input, that's fine. For one that runs many independent test cases in a loop, call dp.cache_clear() between cases or you'll get answers from a previous test case's state bleeding in. This is exactly as fun to debug as it sounds.

For recursion time complexity, @cache collapses an exponential call tree into a DAG. You pay each unique subproblem exactly once.


Arithmetic Idioms

Integer Division Trips You Up Across Languages

mid = lo + (hi - lo) // 2 # no overflow rows, cols = divmod(idx, n) # flat index to 2D coordinates

// is always floor division in Python 3. -7 // 2 gives -4, not -3. This matters when you're computing indices into negative territory and wondering why your result is off by one.

Python's % always returns a non-negative result when the divisor is positive. -1 % 3 equals 2. Java and C++ return -1 for the same expression. In circular array problems, (i - 1) % n works correctly in Python without an extra + n guard. It does not in most other languages. This difference has caused a lot of silent wrong answers in multilingual interview prep.

divmod(a, b) returns (a // b, a % b) as a tuple. Use it when you need both quotient and remainder, for example converting a flat index to row and column coordinates. Two birds.

Infinity Is Two Characters

import math best = math.inf worst = -math.inf # without import best = float('inf')

Both work in comparisons and arithmetic. math.inf reads slightly better. Use float('inf') if you haven't imported math yet. Either way, stop initializing your minimum to 10**9 and hoping the input fits.


String Idioms

Building Strings in a Loop

result = "".join(chars) result = ", ".join(str(x) for x in arr)

String concatenation in a loop is O(n²) because Python strings are immutable and each + creates a new object. "".join(list) is O(n). This is an interview-visible difference on large inputs. The interviewer won't necessarily call it out during the problem, but it shows up in the complexity discussion. Know what you wrote.

split Behaves Differently Than You Think

words = s.split() # split on any whitespace, collapse multiple spaces words = s.split(",") # split on delimiter s = s.strip() # remove leading and trailing whitespace

s.split() with no argument handles multiple consecutive spaces by treating any run of whitespace as one delimiter. s.split(" ") does not: "a b".split(" ") produces ['a', '', 'b'], which is almost never what you want. For most interview problems, the no-argument form is right. The single-space form is a trap disguised as precision.


Comprehensions and Unpacking

The 2D Initialization Bug

# WRONG: all rows are the same object dp = [[0] * cols] * rows # CORRECT dp = [[0] * cols for _ in range(rows)]

[[0] * cols] * rows creates one list and stores the same reference rows times. Mutating dp[0][0] changes every row. Every single row. At once. Silently. This is the most common silent bug in DP and grid problems, and it produces wrong answers that look completely plausible for small cases. Always use a comprehension for 2D initialization. Burn this into your fingers.

Swap and Destructure Without Extra Variables

a, b = b, a # swap without a temp variable first, *rest = arr # head and tail *init, last = arr # all but last a, _, b = (1, 2, 3) # discard with underscore

Tuple unpacking is zero-cost at runtime. The swap a, b = b, a evaluates the right side first as a tuple, then unpacks. No temp variable, no mid-swap state where both sides are wrong simultaneously. This is one of the genuinely nice things about Python and you should use it freely.


Practice These Under Pressure

Knowing an idiom in isolation is different from reaching for it mid-interview while explaining your approach out loud. SpaceComplexity runs voice-based mock interviews where you code and narrate simultaneously, which is exactly the condition where Python fluency gets tested. Drill these until deque, @cache, and Counter come out without thinking.

For a broader reference, see the Python interview cheat sheet and Python built-in time complexity reference.


Before You Close This Tab

  • enumerate over range(len(arr)) every time. No exceptions. No negotiations.
  • deque.popleft() is non-negotiable for BFS. list.pop(0) is O(n) and will not get better with staring.
  • Counter handles missing keys as zero and supports arithmetic on frequency maps.
  • @cache is one-line memoization. Arguments must be hashable. Call cache_clear() between independent test cases or enjoy debugging phantom state.
  • Python's % returns non-negative results when the divisor is positive. Java and C++ do not. This will matter on a circular array problem at minute 30.
  • The 2D list initialization bug with * is silent and evil. Always use a comprehension for rows.

Further Reading