Five modules, one complexity table, and a dozen traps. This is the part of Python that decides whether your correct idea also runs fast enough.
Interviewers do not test Python trivia. But they do watch you reach for list.pop(0) and quietly note that your O(n) algorithm just became O(n²). This page is the small set of facts that stops that happening.
Know this table. Most accidental time-limit failures are one row of it.
| Operation | Cost | Note |
|---|---|---|
lst[i], lst[i] = x, len(lst) | O(1) | |
lst.append(x), lst.pop() | O(1) | Amortised. A resize happens occasionally. |
lst.insert(0, x), lst.pop(0) | O(n) | Everything shifts. Use a deque. |
x in lst | O(n) | The most common accidental slowdown. |
x in set, x in dict | O(1) | Average. O(n) worst case on hash collisions. |
lst.sort(), sorted(it) | O(n log n) | Timsort. Stable, and O(n) on already-sorted input. |
lst[i:j] | O(j - i) | It copies. Slicing in a loop hides a quadratic. |
s1 + s2 for strings | O(len) | Strings are immutable, so it builds a new one. |
"".join(parts) | O(total) | One allocation. Always prefer this. |
deque.popleft(), appendleft() | O(1) | The queue for BFS. |
heappush, heappop | O(log n) | Min-heap only. |
heapify(lst) | O(n) | Cheaper than n pushes. |
bisect_left, bisect_right | O(log n) | On a list you already keep sorted. |
insort | O(n) | The search is log, the insert shifts. Easy to misjudge. |
min(lst), max(lst), sum(lst) | O(n) | Calling one inside a loop is a hidden quadratic. |
set(a) & set(b) | O(min(len)) | Union, difference and intersection are all cheap. |
dict.items(), keys() | O(1) | A view, not a copy. Iterating it is O(n). |
copy.deepcopy(x) | O(size) | Slow. Almost never what you want in an interview. |
x in lst inside a loop over the same list, and lst.pop(0) as a queue. Both turn a linear algorithm into a quadratic one, both look completely innocent, and both are caught instantly by a reviewer.Three classes carry almost all the weight.
from collections import Counter
counts = Counter("mississippi")
counts["s"] # 4
counts["z"] # 0, and no KeyError
counts.most_common(2) # [('i', 4), ('s', 4)]
# Counters compare and combine directly, which anagram problems love.
Counter("listen") == Counter("silent") # True
Counter("aab") - Counter("ab") # Counter({'a': 1})
Two behaviours matter. A missing key reads as 0 rather than raising, and most_common(k) uses heapq.nlargest internally, so it is O(n log k) rather than a full sort.
from collections import defaultdict
graph: defaultdict[str, list[str]] = defaultdict(list)
graph["a"].append("b") # no "if key not in graph" needed
tally: defaultdict[str, int] = defaultdict(int)
tally["x"] += 1
buckets: defaultdict[str, set[str]] = defaultdict(set)
buckets["k"].add("v")
len(graph) can grow just from looking. If you need to check without creating, use key in graph or graph.get(key).from collections import deque
queue = deque([1, 2, 3])
queue.popleft() # O(1). list.pop(0) would be O(n).
queue.appendleft(0)
queue.extendleft([9, 8]) # note: inserts in reverse order
recent = deque(maxlen=3) # fixed size; pushing past 3 drops from the far end
Use a deque for every BFS queue, for sliding-window maximum, and any time you need both ends. It is a doubly linked list of blocks, so indexing the middle is O(n). If you index it a lot, you wanted a list.
import heapq
heap = [5, 1, 4]
heapq.heapify(heap) # O(n), in place
heapq.heappush(heap, 0)
smallest = heapq.heappop(heap) # 0
heapq.heapreplace(heap, 9) # pop then push, one sift instead of two
heapq.nlargest(2, [5, 1, 4]) # [5, 4]
heapq.nsmallest(2, [5, 1, 4]) # [1, 4]
| Need | Do this |
|---|---|
| A max-heap | Push -value. There is no max-heap in the standard library. |
| Order by a key | Push tuples: (key, tiebreak, payload). |
| Top k, quickly | nlargest / nsmallest. They already do the size-k heap trick. |
| Delete an arbitrary item | Not supported. Mark it dead and skip it when it surfaces. |
TypeError. Always put something comparable second, such as an insertion counter, and keep unorderable payloads third or later. See Pattern 9.import bisect
data = [1, 3, 3, 5]
bisect.bisect_left(data, 3) # 1, first index where data[i] >= 3
bisect.bisect_right(data, 3) # 3, first index where data[i] > 3
bisect.bisect_left(data, 4) # 3, the insertion point for a missing value
bisect.insort(data, 4) # data becomes [1, 3, 3, 4, 5]
bisect_left is lower bound and bisect_right is upper bound. The count of values equal to x is bisect_right(data, x) - bisect_left(data, x). Both return an insertion point, never a promise that the value is present, so always check data[i] == x before trusting it. That check is the bug in Find First and Last Position.Since Python 3.10 both accept a key= argument, which removes the old trick of building a parallel list of keys.
from itertools import accumulate, combinations, permutations, product, groupby, pairwise
list(accumulate([1, 2, 3, 4])) # [1, 3, 6, 10] prefix sums
list(accumulate([3, 1, 4], max)) # [3, 3, 4] running maximum
list(combinations([1, 2, 3], 2)) # [(1, 2), (1, 3), (2, 3)]
list(permutations([1, 2])) # [(1, 2), (2, 1)]
list(product([0, 1], repeat=2)) # [(0,0), (0,1), (1,0), (1,1)]
list(pairwise([1, 2, 3])) # [(1, 2), (2, 3)] adjacent pairs
[(key, len(list(run))) for key, run in groupby("aaabbc")] # [('a',3), ('b',2), ('c',1)]
| Function | Use it for |
|---|---|
accumulate | Prefix sums in one line. See Pattern 13. |
pairwise | Comparing adjacent elements without index arithmetic. Python 3.10+. |
groupby | Run-length encoding. It only groups consecutive equal items, so sort first if you meant to group globally. |
combinations, permutations | Checking a brute force against your backtracking answer. |
product | Nested loops over a grid of choices, flattened into one loop. |
itertools as what you would ship. Reaching for the library first reads as avoiding the question.from functools import cache, lru_cache, reduce, cmp_to_key
@cache
def fib(n: int) -> int:
"""Without the decorator this is 2**n calls. With it, n."""
return n if n < 2 else fib(n - 1) + fib(n - 2)
fib(100) # instant
reduce(lambda a, b: a * b, [1, 2, 3, 4]) # 24
@cache is the fastest route from a brute-force recursion to a working dynamic programming solution. Two constraints: the arguments must be hashable, so pass tuples rather than lists, and the cache lives as long as the function, so define the helper inside the outer function to avoid results leaking between calls.lru_cache(maxsize=None) is the same thing on older versions. cmp_to_key converts an old-style comparison function into a sort key, which is occasionally the cleanest way to express a custom ordering such as Largest Number.
def add(item, bucket=[]):
# The list is created ONCE, when
# the function is defined. Every
# call shares it.
bucket.append(item)
return bucket
def add(item, bucket=None):
if bucket is None:
bucket = []
bucket.append(item)
return bucket
grid = [[0] * 3] * 2
grid[0][0] = 1
# grid is now [[1,0,0], [1,0,0]]
# because both rows are the SAME
# list object.
grid = [[0] * 3 for _ in range(2)]
grid[0][0] = 1
# grid is [[1,0,0], [0,0,0]]
# The comprehension builds a fresh
# row each time.
| Trap | What goes wrong | Fix |
|---|---|---|
lst.pop(0) as a queue | O(n) per pop, so O(n²) overall | collections.deque |
result += char in a loop | Rebuilds the string each time, O(n²) | Collect into a list, then "".join(parts) |
x in lst inside a loop | O(n) per check | Build a set first |
| Mutating a list while iterating it | Skipped elements, silently | Iterate a copy, or build a new list |
-7 // 2 is -4 | Floor division rounds toward minus infinity | int(-7 / 2) for truncation, or math.trunc |
-7 % 3 is 2 | Different from C and Java, which give -1 | Usually what you want. Say so out loud. |
0.1 + 0.2 != 0.3 | Floating point | Stay in integers, or use math.isclose |
is for value comparison | Works for small ints by accident, then stops | == for values, is only for None and identity |
| Recursion past ~1000 frames | RecursionError | Rewrite iteratively, or sys.setrecursionlimit and say why |
sorted(d) on a dict | Sorts the keys, not the items | sorted(d.items(), key=...) |
| Slicing inside a loop | s[i:] copies, so a linear loop turns quadratic | Pass indices instead of slices |
from collections import Counter
# Iterate with an index, or two sequences together.
for index, value in enumerate("abc"):
pass
for a, b in zip([1, 2], "xy"):
pass
# Sort by a computed key. sorted() returns a new list; .sort() is in place.
words = ["bbb", "a", "cc"]
by_length = sorted(words, key=len) # ['a', 'cc', 'bbb']
by_two = sorted(words, key=lambda w: (len(w), w)) # length, then alphabetical
# Descending without reversing afterwards.
descending = sorted([3, 1, 2], reverse=True)
# max and min take a key too.
longest = max(words, key=len)
# Unpacking, including the starred form.
first, *rest = [1, 2, 3]
head, *middle, tail = [1, 2, 3, 4]
# Swap without a temporary. The right side is evaluated first.
a, b = 1, 2
a, b = b, a
# Chained comparison, which reads exactly like the maths.
n = 5
in_range = 0 <= n < 10
# any and all short-circuit.
has_even = any(x % 2 == 0 for x in [1, 3, 4])
all_positive = all(x > 0 for x in [1, 2])
# Dict and set comprehensions.
squares = {x: x * x for x in range(3)}
letters = {ch for ch in "hello"}
# The walrus operator, for "compute once, test, then reuse".
values = [1, 2, 3]
if (total := sum(values)) > 5:
leftover = total - 5
# Counting without a loop.
most_common_char, _ = Counter("aabbbcc").most_common(1)[0]
# A grid of the four cardinal moves, used in every BFS and DFS on a grid.
DIRECTIONS = ((1, 0), (-1, 0), (0, 1), (0, -1))
| Fact | Why it matters in an interview |
|---|---|
| Integers are arbitrary precision | No overflow, ever. Say so when the interviewer is thinking in C++ or Java, where your sum would need a 64-bit type. |
| Default recursion limit is about 1000 | A DFS on a 200×200 grid can exceed it. Know the iterative form. |
| Strings are immutable | Every edit builds a new string. Build a list of characters and join at the end. |
| Sort is stable | Equal keys keep their input order, so you can sort by two keys in two passes, least significant first. |
| Dicts keep insertion order | Guaranteed since 3.7. Useful for deterministic output, and for a simple LRU. |
| Sets and dicts have no order guarantee across runs for arbitrary objects | If your output depends on set iteration order, it is not deterministic. Sort before returning. |
| Python is roughly 10 to 100 times slower than C++ | Assume about 10⁷ simple operations per second, not 10⁸. See Guide 2. |
| No built-in balanced BST | There is no std::map equivalent. Use sortedcontainers if allowed, or a heap plus lazy deletion, or say what you would use. |
deque for queues, never list.pop(0). set for membership, never in list.Counter, defaultdict, heapq, bisect, @cache. Five tools cover most problems."".join(parts), never repeated string concatenation.[[0] * n for _ in range(m)], never [[0] * n] * m.