Pattern 19 · Reference

Python Tricks

Clean, idiomatic Python saves time and signals competence. Knowing the standard library means fewer bugs and faster implementations — this is the reference for the moves that answer “can you do that more cleanly?” before it's asked.

modules 5 coresnippets 40+pitfalls 4 classicpractice 7 problems
01 collections

Counter, defaultdict, deque

The collections module is the single highest-value import in interview Python. These three cover frequency counting, graph building, and O(1) queues.

C1Counter — frequency in one line

counter.pypython
from collections import Counter

# Basic frequency counting
nums = [1, 2, 2, 3, 3, 3]
count = Counter(nums)          # Counter({3: 3, 2: 2, 1: 1})

# Top k frequent
count.most_common(2)           # [(3, 3), (2, 2)]

# Arithmetic operations
a = Counter("aabbc")
b = Counter("abc")
a - b                          # Counter({'a': 1, 'b': 1})
a + b                          # Counter({'a': 3, 'b': 3, 'c': 2})
a & b                          # Intersection: Counter({'a': 1, 'b': 1, 'c': 1})
a | b                          # Union: Counter({'a': 2, 'b': 2, 'c': 1})

# Check if anagram
def is_anagram(s, t):
    return Counter(s) == Counter(t)

# Top K Frequent Elements (LC 347)
def topKFrequent(nums, k):
    return [x for x, _ in Counter(nums).most_common(k)]

C2defaultdict — no more KeyError dances

defaultdict.pypython
from collections import defaultdict

# Auto-initialize with default value
graph = defaultdict(list)       # missing key -> []
graph[0].append(1)              # no KeyError

counts = defaultdict(int)       # missing key -> 0
counts['a'] += 1                # no need to check existence

groups = defaultdict(set)       # missing key -> set()

# Building an adjacency list
def build_graph(edges):
    graph = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)
        graph[v].append(u)
    return graph

# Group Anagrams (LC 49)
def groupAnagrams(strs):
    groups = defaultdict(list)
    for s in strs:
        key = tuple(sorted(s))    # or use Counter -> tuple
        groups[key].append(s)
    return list(groups.values())

C3deque — O(1) at both ends

deque.pypython
from collections import deque

# O(1) operations on both ends
dq = deque()
dq.append(1)         # right: [1]
dq.appendleft(0)     # left:  [0, 1]
dq.pop()             # right: returns 1, deque is [0]
dq.popleft()         # left:  returns 0, deque is []

# BFS template
def bfs(graph, start):
    queue = deque([start])
    visited = {start}
    while queue:
        node = queue.popleft()       # O(1) vs list.pop(0) which is O(n)!
        for neighbor in graph[node]:
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)

# Sliding window maximum (LC 239)
def maxSlidingWindow(nums, k):
    dq = deque()       # stores indices, front = max
    result = []
    for i, num in enumerate(nums):
        while dq and nums[dq[-1]] <= num:
            dq.pop()
        dq.append(i)
        if dq[0] <= i - k:           # remove out-of-window
            dq.popleft()
        if i >= k - 1:
            result.append(nums[dq[0]])
    return result
Operationlistdeque
append rightO(1)*O(1)
pop rightO(1)O(1)
append leftO(n)O(1)deque wins
pop leftO(n)O(1)deque wins
index accessO(1)O(n)list wins

* amortized

02 heapq

heapq — always a MIN heap

Python's heapq is a min-heap by default — the smallest element sits at index 0. There is no max-heap; you fake one by negating values.

heapq_basics.pypython
import heapq

# Basic operations
heap = []
heapq.heappush(heap, 3)
heapq.heappush(heap, 1)
heapq.heappush(heap, 2)
heapq.heappop(heap)             # returns 1 (smallest)

# Heapify existing list: O(n)
nums = [5, 3, 1, 4, 2]
heapq.heapify(nums)             # nums is now a min-heap

# Peek without popping
smallest = heap[0]

# MAX heap trick: negate all values
max_heap = []
heapq.heappush(max_heap, -5)
heapq.heappush(max_heap, -3)
biggest = -heapq.heappop(max_heap)   # returns 5

# nlargest / nsmallest shortcuts
heapq.nlargest(3, nums)         # top 3 largest
heapq.nsmallest(3, nums)        # top 3 smallest
heapq_patterns.py — LC 215, LC 23python
# Kth Largest Element (LC 215)
def findKthLargest(nums, k):
    heap = nums[:k]
    heapq.heapify(heap)
    for num in nums[k:]:
        if num > heap[0]:
            heapq.heapreplace(heap, num)  # pop + push in one operation
    return heap[0]

# Merge K Sorted Lists (LC 23)
def mergeKLists(lists):
    heap = []
    for i, node in enumerate(lists):
        if node:
            heapq.heappush(heap, (node.val, i, node))

    dummy = curr = ListNode(0)
    while heap:
        val, i, node = heapq.heappop(heap)
        curr.next = node
        curr = curr.next
        if node.next:
            heapq.heappush(heap, (node.next.val, i, node.next))
    return dummy.next

Custom priorities via tuples

heap_tuples.pypython
# Tuples are compared element by element
# (priority, tiebreaker, value)
heap = []
heapq.heappush(heap, (2, 0, "low"))
heapq.heappush(heap, (1, 1, "high"))
heapq.heappop(heap)   # (1, 1, "high") - smallest first element
The tiebreaker matters
In the merge-k example above, the index i is the tiebreaker — without it, Python would try to compare two ListNode objects when values tie, and crash.
03 bisect

bisect — binary search, pre-written

Binary search on sorted lists without writing the loop (and without the off-by-one bugs). The two variants differ only in how they treat equal elements.

bisect.pypython
import bisect

arr = [1, 3, 5, 7, 9]

# bisect_left: leftmost position to insert (first element >= target)
bisect.bisect_left(arr, 5)     # 2  (index of 5)
bisect.bisect_left(arr, 4)     # 2  (would go before 5)

# bisect_right: rightmost position to insert (first element > target)
bisect.bisect_right(arr, 5)    # 3  (after 5)

# insort: insert while maintaining sort order
bisect.insort(arr, 4)          # arr = [1, 3, 4, 5, 7, 9]

# Count elements in range [lo, hi]
def count_in_range(arr, lo, hi):
    left = bisect.bisect_left(arr, lo)
    right = bisect.bisect_right(arr, hi)
    return right - left

# Binary search for exact match
def binary_search(arr, target):
    i = bisect.bisect_left(arr, target)
    return i < len(arr) and arr[i] == target
bisect_left(arr, 5) bisect_right(arr, 5) | | v v [1, 3, 5, 7, 9] [1, 3, 5, 7, 9] ^ ^ inserts BEFORE 5 inserts AFTER 5
04 itertools & functools

Combinatorics and memoization for free

I1itertools — the combinatorics toolbox

itertools.pypython
from itertools import combinations, permutations, product, accumulate, groupby

# Combinations: choose k from n (order doesn't matter)
list(combinations([1,2,3], 2))       # [(1,2), (1,3), (2,3)]

# Permutations: arrange k from n (order matters)
list(permutations([1,2,3], 2))       # [(1,2),(1,3),(2,1),(2,3),(3,1),(3,2)]

# Product: cartesian product (nested loops)
list(product([0,1], repeat=3))       # all 3-bit binary numbers
# [(0,0,0), (0,0,1), (0,1,0), (0,1,1), (1,0,0), (1,0,1), (1,1,0), (1,1,1)]

# Accumulate: prefix sums (and more)
list(accumulate([1,2,3,4]))          # [1, 3, 6, 10]
# With custom function
list(accumulate([3,1,4,1], max))     # [3, 3, 4, 4] running max

# Groupby: group consecutive identical elements
data = sorted([('a',1),('b',2),('a',3)], key=lambda x: x[0])
for key, group in groupby(data, key=lambda x: x[0]):
    print(key, list(group))
# a [('a',1), ('a',3)]
# b [('b',2)]
itertools_interview.pypython
# Generate all subsets using combinations
def subsets(nums):
    result = []
    for k in range(len(nums) + 1):
        result.extend(combinations(nums, k))
    return result

# Prefix sum array for range queries
from itertools import accumulate
prefix = list(accumulate(nums, initial=0))
# Sum of nums[i:j] = prefix[j] - prefix[i]

F1@lru_cache — top-down DP in three lines

lru_cache.py — LC 62, LC 1143python
from functools import lru_cache

# Fibonacci - DP with zero effort
@lru_cache(maxsize=None)
def fib(n):
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)

# Grid DP: Unique Paths (LC 62)
@lru_cache(maxsize=None)
def uniquePaths(m, n):
    if m == 1 or n == 1:
        return 1
    return uniquePaths(m - 1, n) + uniquePaths(m, n - 1)

# Longest Common Subsequence (LC 1143)
def longestCommonSubsequence(text1, text2):
    @lru_cache(maxsize=None)
    def dp(i, j):
        if i == len(text1) or j == len(text2):
            return 0
        if text1[i] == text2[j]:
            return 1 + dp(i + 1, j + 1)
        return max(dp(i + 1, j), dp(i, j + 1))
    return dp(0, 0)
@lru_cacheExplicit DP table
Easier to write (top-down)Bottom-up, often faster
Good for trees, recursionGood for 1D/2D arrays
Handles sparse states wellBetter space optimization
Watch out: mutable args failNo such restriction
Clear cache: func.cache_clear()No cleanup needed

F2reduce

reduce.pypython
from functools import reduce

# Product of all elements
product = reduce(lambda a, b: a * b, [1, 2, 3, 4])  # 24

# Flatten nested list
flat = reduce(lambda a, b: a + b, [[1,2], [3,4], [5]])  # [1,2,3,4,5]
# Better: [x for sublist in nested for x in sublist]
05 array & string moves

The everyday tricks

T1Matrix creation — the classic trap

matrix_creation.pypython
# WRONG - all rows share the same list object!
matrix = [[0] * 3] * 3
matrix[0][0] = 1
# matrix is now [[1,0,0], [1,0,0], [1,0,0]]  <-- ALL rows changed

# CORRECT - each row is an independent list
matrix = [[0] * 3 for _ in range(3)]
matrix[0][0] = 1
# matrix is now [[1,0,0], [0,0,0], [0,0,0]]  <-- only row 0 changed
WRONG: [[0]*3] * 3 matrix[0] ----\ matrix[1] -----+--> [0, 0, 0] (SAME object!) matrix[2] ----/ CORRECT: [[0]*3 for _ in range(3)] matrix[0] --> [0, 0, 0] matrix[1] --> [0, 0, 0] (different objects) matrix[2] --> [0, 0, 0]
1 / 6walkthrough — what does the aliased matrix print?
0r0
0
0
0r1
0
0
0r2
0
0
rows 3
list objects 1 (!)
m = [[0]*3] * 3 — shown flattened, three rows of three. It looks like a fresh 3×3 grid… but the outer * 3 copied the reference, not the list. All three rows point at the same object.

T2zip for transpose (and rotation)

transpose.pypython
matrix = [[1,2,3],
          [4,5,6],
          [7,8,9]]

transposed = list(zip(*matrix))
# [(1,4,7), (2,5,8), (3,6,9)]

# As list of lists
transposed = [list(row) for row in zip(*matrix)]

# Rotate 90 degrees clockwise
rotated = [list(row) for row in zip(*matrix[::-1])]
# [[7,4,1], [8,5,2], [9,6,3]]

T3enumerate with start · sorted with key

enumerate_sort.pypython
for i, val in enumerate(arr, start=1):
    print(f"Item {i}: {val}")        # starts from 1, not 0

# Sort by custom criteria
intervals = [[1,3], [2,6], [0,1]]
intervals.sort(key=lambda x: x[0])             # by start time

# Sort by multiple keys
students = [("Alice", 90), ("Bob", 85), ("Charlie", 90)]
students.sort(key=lambda x: (-x[1], x[0]))     # score desc, name asc

# Sort dict by value
d = {'a': 3, 'b': 1, 'c': 2}
sorted(d.items(), key=lambda x: x[1])           # [('b',1), ('c',2), ('a',3)]

T4String building — join, never +=

string_building.pypython
# WRONG - O(n^2) because strings are immutable
result = ""
for s in strings:
    result += s           # creates new string each time

# CORRECT - O(n) total
result = "".join(strings)

# Also good for building character by character
chars = []
for ...:
    chars.append(ch)
result = "".join(chars)
Concatenation with += join() Step 1: "a" Collect: ["a", "b", "c", "d"] Step 2: "a" + "b" = "ab" One pass: "abcd" Step 3: "ab" + "c" = "abc" Step 4: "abc" + "d" = "abcd" Total chars copied: 1+2+3+4 Total chars copied: 4 = O(n^2) = O(n)
06 one-liners

Quick wins & built-ins

Idioms worth having at your fingertips — each one replaces three to five lines of noise.

one_liners.pypython
# Swap variables
a, b = b, a

# Infinity
pos_inf = float('inf')
neg_inf = float('-inf')

# Flatten 2D list
flat = [x for row in matrix for x in row]

# Conditional expression
x = a if condition else b

# Check all / any
all(x > 0 for x in nums)      # True if all positive
any(x > 0 for x in nums)      # True if at least one positive

# Dictionary comprehension
squares = {x: x**2 for x in range(5)}

# Set operations
a = {1, 2, 3}
b = {2, 3, 4}
a | b    # union:        {1, 2, 3, 4}
a & b    # intersection: {2, 3}
a - b    # difference:   {1}
a ^ b    # symmetric:    {1, 4}

# Unpack with *
first, *middle, last = [1, 2, 3, 4, 5]
# first=1, middle=[2,3,4], last=5

# Walrus operator (Python 3.8+)
while (line := input()) != "quit":
    process(line)

# Count digits
from math import log10, floor
digits = floor(log10(n)) + 1 if n > 0 else 1

# Reverse anything
"hello"[::-1]           # "olleh"
[1,2,3][::-1]           # [3, 2, 1]

# Divmod
quotient, remainder = divmod(17, 5)   # (3, 2)

# Multiple assignment
x = y = z = 0

# Chained comparison
if 0 <= x < n and 0 <= y < m:    # bounds check
    pass
builtins.pypython
# Math
abs(-5)                    # 5
max(a, b, c)               # largest
min(a, b, c)               # smallest
pow(2, 10)                 # 1024
pow(2, 10, 1000)           # 24 (modular exponentiation!)
sum(iterable)              # sum all elements
divmod(17, 5)              # (3, 2)

# Type conversion
int("42")                  # 42
str(42)                    # "42"
list("abc")                # ['a', 'b', 'c']
set([1,1,2])               # {1, 2}
tuple([1,2])               # (1, 2)
ord('a')                   # 97
chr(97)                    # 'a'

# Boolean checks
isinstance(x, int)         # type check
bool([])                   # False (empty collections are falsy)
bool([0])                  # True  (non-empty is truthy)

# Useful for interviews
float('inf')               # positive infinity
float('-inf')              # negative infinity
07 common pitfalls

Where Python bites back

Mistake 1 — mutable default arguments
Default argument values are evaluated once, at function definition — a default [] or {} persists and accumulates across calls.
mutable_default.pypython
# WRONG
def append_to(element, target=[]):
    target.append(element)
    return target

append_to(1)   # [1]
append_to(2)   # [1, 2]  <-- BUG! Default list persists between calls

# CORRECT
def append_to(element, target=None):
    if target is None:
        target = []
    target.append(element)
    return target
Mistake 2 — the list multiplication trap
[[0] * 3] * 3 creates three references to the same inner list — see the walkthrough in section 05. It applies to every mutable: [{}] * 3 is three references to one dict.
list_multiplication.pypython
# Creates 3 references to the SAME inner list
bad = [[0] * 3] * 3

# Creates 3 independent inner lists
good = [[0] * 3 for _ in range(3)]

# This also applies to other mutables
bad_dict = [{}] * 3        # 3 refs to same dict
good_dict = [{} for _ in range(3)]
Mistake 3 — comparing values with is
Python caches small integers (−5 to 256), so is seems to work for values — until it doesn't. is compares identity; always use == for value comparison.
integer_caching.pypython
# Python caches integers from -5 to 256
a = 256
b = 256
a is b    # True (cached)

a = 257
b = 257
a is b    # False (not cached)

# ALWAYS use == for value comparison, not 'is'
Mistake 4 — shallow copy of nested structures
list.copy() and list(original) copy only the outer list — the inner lists are still shared. Nested structures need copy.deepcopy.
shallow_vs_deep.pypython
import copy

original = [[1, 2], [3, 4]]

shallow = original.copy()            # or list(original)
shallow[0][0] = 99
# original is now [[99, 2], [3, 4]]  <-- inner lists are shared!

original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original)
deep[0][0] = 99
# original is still [[1, 2], [3, 4]]  <-- fully independent
08 complexity reference

What Python operations actually cost

The interviewer will ask. Know these cold — especially the ones marked as traps.

List

OperationTimeNotes
append(x)O(1)*Amortized
pop()O(1)From end
pop(0)O(n)From front — use deque instead!
insert(i, x)O(n)Shifts elements
x in listO(n)Linear scan
list[i]O(1)Index access
list[i:j]O(j−i)Slice creates a new list
sort()O(n log n)Timsort
len(list)O(1)Stored as attribute

Dict & set

OperationTimeNotes
d[key] / d[key] = valO(1)*Average case
key in dO(1)*Average case
del d[key]O(1)*Average case
d.keys()O(1)Returns a view
for k in dO(n)Iteration
set.add(x) / x in set / remove(x)O(1)*Average case
a | b (union)O(m+n)Both sets
a & b (intersection)O(min(m,n))Checks the smaller set

String

OperationTimeNotes
s + tO(n+m)Creates a new string
s[i]O(1)Index access
s in tO(n·m)Substring search
''.join(list)O(n)MUCH better than repeated +
s[::-1]O(n)Reverse

heapq

OperationTimeNotes
heappushO(log n)
heappopO(log n)
heapifyO(n)NOT O(n log n)
heapreplaceO(log n)Pop + push in one step
nlargest(k, arr)O(n log k)Better than sorting if k << n
Final tips
Use collections freely — Counter, defaultdict, deque are interview staples. @lru_cache is your DP shortcut. heapq is always min-heap — negate for max. join(), not +=, for strings. List comprehensions over map/filter. enumerate over range(len(...)), zip for parallel iteration, sorted(key=...) for custom orders. Test edge cases — and when in doubt, use a dict: O(1) lookup solves half of all problems.
10 practice

Practice ladder

In this order — easiest first. Tick each one as you solve it, and re-solve anything that took more than 25 minutes two days later.

7 problems
mediumLC 347
Counter + most_common(k). One line.
mediumLC 49
defaultdict(list) keyed by tuple(sorted(s)).
mediumLC 215
Min-heap of size k + heapreplace.
mediumLC 62
@lru_cache grid DP — three lines of recursion.
mediumLC 1143
@lru_cache on dp(i, j) over two sequences.
hardLC 23
heapq with (val, i, node) tuples — the tiebreaker matters.
hardLC 239
deque as a monotonic queue — O(1) at both ends.