Skip to content
ΣDSA Patterns
Menu
Language

Language

Python for interviews

Idioms, collections, ListNode/TreeNode, and the gotchas that show up in coding interviews. Python 3.10+; site CI runs 3.12.

Assumes Python 3.10 or newer. The site CI runs 3.12.

match needs 3.10. dict | other and list[int] need 3.9. The walrus operator needs 3.8. math.lcm and int.bit_count need 3.9 and 3.10.

Each Python block below runs on its own.

Names and unpacking

a, b = 1, 2
a, *rest = [1, 2, 3]
assert rest == [2, 3]
a, b = b, a
assert (a, b) == (2, 1)
x = y = z = 0
assert (x, y, z) == (0, 0, 0)

type(x) returns the class. isinstance(x, (int, float)) accepts either. int("5"), float("1.5"), str(5), bool(0), and list("ab") are the conversions you actually want. bool(0) is False.

Operators

assert 7 / 2 == 3.5
assert 7 // 2 == 3
assert -3 // 2 == -2          # floor, toward -infinity
assert -3 % 2 == 1            # remainder takes the sign of the divisor
assert 2 ** 10 == 1024
assert (1 < 3 < 5) is True
value = 1 if True else 2
assert value == 1

and, or, and not are the boolean operators. in and is are the membership and identity operators. & | ^ ~ << >> are bitwise. See the bit-manipulation pattern for the identities that matter in problems.

x or default returns default for every falsy x, including 0, "", and []. Use x if x is not None else default when those values are real data.

name = "ada"
n = len(name)
assert (n := len(name)) > 2 and n == 3

The parentheses around the walrus are required inside an if test.

Strings

name, pi, bits = "ada", 3.14159, 10
text = f"{name!r:>6} {pi:.2f} {1024:,} {bits:08b} {bits=}"
assert text == " 'ada' 3.14 1,024 00001010 bits=10"

s = "ab,cd"
assert s[0] == "a" and s[-1] == "d"
assert s[1:4] == "b,c" and s[::-1] == "dc,ba"
assert s.split(",") == ["ab", "cd"]
assert " ".join(["ab", "cd"]) == "ab cd"
assert s.replace("ab", "xy") == "xy,cd"
assert s.startswith("ab") and s.endswith("cd")
assert s.find("z") == -1
assert "5".isdigit() and "a".isalpha()
assert ord("A") == 65 and chr(65) == "A"

s.index("z") raises ValueError when the substring is missing. s.find returns -1. s.split() with no argument splits on whitespace and drops empty pieces. s.strip, lstrip, and rstrip remove ends only.

A format width is a minimum. {10:08b} is 00001010. {1024:08b} is 10000000000, because 1024 already needs 11 bits. !r counts the quotes toward the width, so {name!r:>6} for ada is a single space plus 'ada'.

Lists

bad = [[0] * 2] * 2
bad[0][0] = 1
assert bad == [[1, 0], [1, 0]]

good = [[0] * 2 for _ in range(2)]
good[0][0] = 1
assert good == [[1, 0], [0, 0]]

nums = [3, 1, 2]
nums.append(4)
nums.extend([5])
assert nums == [3, 1, 2, 4, 5]
assert nums.pop() == 5
assert nums == [3, 1, 2, 4]
assert nums[1:3] == [1, 2]
assert sorted(nums) == [1, 2, 3, 4]
assert nums[::-1] == [4, 2, 1, 3]
shallow = nums[:]
shallow.append(9)
assert 9 not in nums

list.sort sorts in place and returns None. sorted returns a new list. nums.pop(0) and nums.insert(0, x) are O(n). Use collections.deque when both ends matter.

copy.copy is shallow. copy.deepcopy copies nested containers.

Tuples and sets

pair = (1, 2)
one = (1,)
assert one != (1)

items = {1, 2, 3}
empty = set()
items.add(4)
items.discard(9)
assert items == {1, 2, 3, 4}
a, b = {1, 2}, {2, 3}
assert (a | b, a & b, a - b, a ^ b) == ({1, 2, 3}, {2}, {1}, {1, 3})
assert {1} <= a
assert frozenset([1, 2]) == frozenset([2, 1])

{} is a dict. An empty set is set(). remove raises KeyError when the element is missing. discard does not.

Dicts

keys, values = ["a", "b"], [1, 2]
d = dict(zip(keys, values))
assert d.get("z", 0) == 0
d.setdefault("k", []).append(1)
assert d["k"] == [1]
left, right = {"a": 1}, {"b": 2}
assert left | right == {"a": 1, "b": 2}
assert {k: v for k, v in d.items() if k == "a"} == {"a": 1}

del d[key] raises KeyError when the key is missing. d.pop(key, None) returns the default. Do not add or remove keys while iterating the dict.

Comprehensions

assert [x * x for x in range(5) if x % 2 == 0] == [0, 4, 16]
matrix = [[1, 2], [3]]
assert [y for row in matrix for y in row] == [1, 2, 3]
squares = (x * x for x in range(3))
assert list(squares) == [0, 1, 4]

The parentheses form is a generator. It yields one value at a time and is exhausted after one pass.

Control flow

seen = []
for i, value in enumerate(["a", "b"], start=1):
    seen.append((i, value))
assert seen == [(1, "a"), (2, "b")]
assert list(zip([1, 2], ["a", "b", "c"])) == [(1, "a"), (2, "b")]

found = None
for value in [1, 3, 4]:
    if value % 2 == 0:
        found = value
        break
else:
    found = -1
assert found == 4

The for/else clause runs when the loop did not break.

def classify(cmd):
    match cmd:
        case "go" | "run":
            return "move"
        case [x, y]:
            return ("pair", x, y)
        case {"k": v}:
            return ("map", v)
        case int() if cmd > 5:
            return "big"
        case _:
            return "other"

assert classify("run") == "move"
assert classify([1, 2]) == ("pair", 1, 2)
assert classify({"k": 3}) == ("map", 3)
assert classify(6) == "big"
assert classify(1) == "other"

Functions

def append_bad(value, bucket=[]):
    bucket.append(value)
    return bucket

first = append_bad(1)
second = append_bad(2)
assert first is second and second == [1, 2]

def append_ok(value, bucket=None):
    if bucket is None:
        bucket = []
    bucket.append(value)
    return bucket

assert append_ok(1) == [1]
assert append_ok(2) == [2]

fns = [lambda: i for i in range(3)]
assert [fn() for fn in fns] == [2, 2, 2]
fns = [lambda i=i: i for i in range(3)]
assert [fn() for fn in fns] == [0, 1, 2]

def outer():
    count = 0

    def inner():
        nonlocal count
        count += 1
        return count

    return inner

tick = outer()
assert tick() == 1 and tick() == 2

A default argument is evaluated once, at definition. A list, dict, or set default is shared by every caller. A closure over a loop variable sees the variable’s final value unless the value is bound as a default.

def f(a, b=2, /, c=3, *args, d, e=5, **kw):
    return (a, b, c, args, d, e, kw)

assert f(1, 2, 3, d=4) == (1, 2, 3, (), 4, 5, {})

Names before / are positional-only. Names after * or *args are keyword-only.

sum(nums, 10) adds a start value. The parameter is keyword-capable: sum(nums, start=10).

Decorators

from functools import wraps

def bold(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        return f"<b>{fn(*args, **kwargs)}</b>"
    return wrapper

def italic(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        return f"<i>{fn(*args, **kwargs)}</i>"
    return wrapper

@bold
@italic
def greet(name):
    return f"Hello, {name}"

assert greet("World") == "<b><i>Hello, World</i></b>"
assert greet.__name__ == "greet"

@bold above @italic means bold(italic(greet)). The call runs bold, then italic, then the function. @wraps copies the original name onto the wrapper. This is a Python function decorator. The Gang-of-Four decorator is an object wrapper.

Iterators

from itertools import groupby

def gen(n):
    yield from range(n)

assert list(gen(3)) == [0, 1, 2]

data = [1, 2, 1]
assert [k for k, _ in groupby(data)] == [1, 2, 1]
data = sorted(data)
groups = [(k, list(g)) for k, g in groupby(data)]
assert groups == [(1, [1, 1]), (2, [2])]

itertools.groupby groups adjacent equal keys only. Sort first when the keys are scattered.

itertools also provides chain, islice, product, permutations, combinations, and accumulate.

Classes, dataclasses, exceptions

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        raise NotImplementedError

class Dog(Animal):
    def speak(self):
        return "woof"

assert Dog("a").speak() == "woof"

from dataclasses import dataclass, field

@dataclass(frozen=True, order=True)
class Point:
    x: int
    y: int = 0
    tags: list = field(default_factory=list)

assert Point(1) < Point(2)

field(default_factory=list) gives each instance its own list. A bare tags: list = [] would be shared, and frozen=True rejects it.

def parse(text):
    try:
        return int(text)
    except ValueError:
        return None

assert parse("3") == 3 and parse("x") is None

else on try runs when no exception was raised. finally runs on the way out. raise New() from err chains the cause.

Files

from pathlib import Path

here = Path("notes.txt")
assert here.suffix == ".txt" and here.stem == "notes"

Open text with encoding="utf-8". pathlib.Path.read_text and write_text cover the small cases. A with block closes the file.

Interview collections

from collections import Counter, defaultdict, deque
import heapq
import bisect

assert Counter("abca").most_common(1) == [("a", 2)]

graph = defaultdict(list)
graph["a"].append("b")
assert graph["missing"] == []

q = deque([1])
q.append(2)
assert q.popleft() == 1

heap = []
heapq.heappush(heap, 3)
heapq.heappush(heap, 1)
assert heapq.heappop(heap) == 1
heapq.heappush(heap, -5)
assert -heapq.heappop(heap) == 5

ordered = [1, 3, 3, 7]
assert bisect.bisect_left(ordered, 3) == 1
assert bisect.bisect_right(ordered, 3) == 3

heapq is a min-heap. Push the negated value for a max-heap. bisect_left is the first index where the value could sit. bisect_right is the index after every equal value.

Math

import math

assert divmod(7, 3) == (2, 1)
assert pow(2, 10, 1000) == 24
assert math.gcd(12, 18) == 6
assert math.lcm(4, 6) == 12
assert math.isclose(0.1 + 0.2, 0.3)
assert round(1.5) == 2 and round(2.5) == 2  # ties go to the even integer
assert math.inf > 10**18

math.prod exists from 3.8. abs(a * b) // math.gcd(a, b) is the LCM on versions without math.lcm.

Identity

assert 1 == 1
sentinel = object()
assert (sentinel is sentinel) is True

is compares identity. Use it for None and for sentinels. == compares values. CPython interns small ints, so is can look like equality there and then fail for larger ints. Compare values with ==.

Async

import asyncio

async def fetch(n):
    return n

async def main():
    return await asyncio.gather(fetch(1), fetch(2))

assert asyncio.run(main()) == [1, 2]

async with and async for are the async forms of the context manager and the loop.

Typing

from typing import TypeVar

T = TypeVar("T")

def first(xs: list[T]) -> T:
    return xs[0]

assert first([1, 2]) == 1

name: str | None = None
assert name is None

On 3.12 or newer the same function can be written def first[T](xs: list[T]) -> T. Protocol, TypedDict, Literal, and Final live in typing.

Standard library names

json, re, random, datetime, logging, subprocess, argparse, csv, sqlite3, unittest, and pytest are the ones that show up outside algorithm problems. re.search, re.findall, and re.sub cover most text work. json.loads / json.dumps cover most JSON. Prefer pathlib over hand-rolled paths.

Linked lists

The linked-list template is this node plus reverse and a dummy-head merge.

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def reverse_list(head):
    prev, cur = None, head
    while cur:
        nxt = cur.next
        cur.next = prev
        prev = cur
        cur = nxt
    return prev

def to_list(head):
    out = []
    while head:
        out.append(head.val)
        head = head.next
    return out

a = ListNode(1, ListNode(2, ListNode(3)))
assert to_list(reverse_list(a)) == [3, 2, 1]

Walk with while cur. A dummy head keeps the merge loop from special-casing the first node. None is the empty list.

Trees

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

def max_depth(root):
    if not root:
        return 0
    return 1 + max(max_depth(root.left), max_depth(root.right))

root = TreeNode(1, TreeNode(2), TreeNode(3, TreeNode(4)))
assert max_depth(root) == 3

Recursion on left and right is the DFS template. Level-order is a queue; snapshot len(q) for the current level.

from collections import deque

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

def level_order(root):
    if not root:
        return []
    res, q = [], deque([root])
    while q:
        level = []
        for _ in range(len(q)):
            n = q.popleft()
            level.append(n.val)
            if n.left:
                q.append(n.left)
            if n.right:
                q.append(n.right)
        res.append(level)
    return res

root = TreeNode(1, TreeNode(2), TreeNode(3))
assert level_order(root) == [[1], [2, 3]]

Graphs

The graph-DFS template stores undirected edges as defaultdict(list). Grid flood fill is a stack (or queue) of (r, c) cells.

from collections import defaultdict

def count_components(n, edges):
    adj = defaultdict(list)
    for a, b in edges:
        adj[a].append(b)
        adj[b].append(a)
    visited = [False] * n
    count = 0

    def dfs(v):
        visited[v] = True
        for u in adj[v]:
            if not visited[u]:
                dfs(u)

    for v in range(n):
        if not visited[v]:
            dfs(v)
            count += 1
    return count

assert count_components(4, [(0, 1), (2, 3)]) == 2
def flood_fill(image, sr, sc, new_color):
    orig = image[sr][sc]
    if orig == new_color:
        return image
    rows, cols = len(image), len(image[0])
    stack = [(sr, sc)]
    while stack:
        r, c = stack.pop()
        if r < 0 or c < 0 or r >= rows or c >= cols or image[r][c] != orig:
            continue
        image[r][c] = new_color
        stack.extend([(r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)])
    return image

assert flood_fill([[1, 1, 1], [1, 1, 0], [1, 0, 1]], 1, 1, 2) == [
    [2, 2, 2],
    [2, 2, 0],
    [2, 0, 1],
]

orig == new_color is the early return that stops an infinite rewrite of the same color.

Union-Find

Path compression plus union by rank. union returns whether the two nodes were in different components.

class UnionFind:
    def __init__(self, n):
        self.parent = list(range(n))
        self.rank = [0] * n
        self.components = n

    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

    def union(self, a, b):
        ra, rb = self.find(a), self.find(b)
        if ra == rb:
            return False
        if self.rank[ra] < self.rank[rb]:
            ra, rb = rb, ra
        self.parent[rb] = ra
        if self.rank[ra] == self.rank[rb]:
            self.rank[ra] += 1
        self.components -= 1
        return True

uf = UnionFind(4)
assert uf.union(0, 1) and uf.union(2, 3)
assert not uf.union(0, 1)
assert uf.components == 2

Trie

class TrieNode:
    def __init__(self):
        self.children = {}
        self.is_word = False

class Trie:
    def __init__(self):
        self.root = TrieNode()

    def insert(self, word):
        node = self.root
        for ch in word:
            if ch not in node.children:
                node.children[ch] = TrieNode()
            node = node.children[ch]
        node.is_word = True

    def _walk(self, s):
        node = self.root
        for ch in s:
            if ch not in node.children:
                return None
            node = node.children[ch]
        return node

    def search(self, word):
        node = self._walk(word)
        return bool(node and node.is_word)

    def startsWith(self, prefix):
        return self._walk(prefix) is not None

t = Trie()
t.insert("apple")
assert t.search("apple") and not t.search("app")
assert t.startsWith("app")

search needs is_word. startsWith only needs the walk to survive.