Python · cheat sheet

Python

Data model and Big-O, functions, closures and decorators, generators, OOP and dataclasses, exceptions, stdlib, typing, the GIL, asyncio and pytest.

Core Python for interviews on one page: the data model, idioms, OOP, the standard library and concurrency, current to Python 3.14.

Objects, identity & copying

Type Mutable Hashable
int, float, bool (an int subclass), str, bytes, frozenset, None no yes
tuple no only if all items are; (1,) needs the comma
list, dict, set, bytearray yes no
  • Variables are names bound to objects; assignment never copies. Arguments are passed by object reference: mutating a passed list is visible to the caller, rebinding the parameter is not.
  • == compares values (__eq__); is compares identity (id()). Use is only for singletons such as None. CPython caches ints -5 to 256 and interns some strings, so is on them can “work” by accident.
  • Shallow copy (copy.copy(x), xs[:], list(xs), d.copy()) is a new container sharing the nested objects; copy.deepcopy(x) copies recursively, cycles included.
import copy
grid = [[0] * 2] * 2                  # two references to ONE row
grid[0][0] = 1                        # [[1, 0], [1, 0]]
grid = [[0] * 2 for _ in range(2)]    # independent rows
a = [[1], [2]]
b = copy.copy(a); b[0].append(9)      # a is now [[1, 9], [2]]
c = copy.deepcopy(a); c[0].append(7)  # a unchanged
python
  • Memory: an object is freed when its reference count hits zero; a generational cycle collector (gc) reclaims cycles. del x removes a name, not the object. Find leaks with tracemalloc.

Collections & Big-O

Operation list dict / set deque
index c[i] O(1) n/a O(1) at ends, O(n) middle
append / pop at end O(1) amortized n/a O(1)
insert / pop at front O(n) n/a O(1)
x in c O(n) O(1) avg, O(n) worst O(n)
get / set / delete key n/a O(1) avg, O(n) worst n/a
insert(i, x), remove(x) O(n) n/a O(n)
sort() O(n log n) n/a n/a
  • Also: len O(1), slice O(k), min/max/sum O(n), heappush/heappop O(log n), heapify O(n).
  • dict: hash table, hashable keys, insertion order guaranteed (3.7). d.get(k, default), d.setdefault(k, []), d1 | d2 (3.9).
  • set: a | b, a & b, a - b, a ^ b, a <= b. {} is an empty dict; use set().
  • xs.sort() sorts in place and returns None; sorted(it) returns a new list. Both are stable (Timsort) and take key= and reverse=.
  • Queues: deque with append + popleft, never list.pop(0).

Comprehensions, slicing & unpacking

evens = [x * x for x in range(10) if x % 2 == 0]   # [0, 4, 16, 36, 64]
index = {ch: i for i, ch in enumerate("ab")}       # {'a': 0, 'b': 1}
flat = [x for row in [[1, 2], [3]] for x in row]   # [1, 2, 3]: outer loop first
labels = ["even" if n % 2 == 0 else "odd" for n in range(3)]
total = sum(x * x for x in range(10**6))           # generator: O(1) memory
if (n := len(evens)) > 3: print(n)                 # walrus (3.8): prints 5
python
  • Dict {k: v ...} and set {...} comprehensions work too; (...) is a lazy, single-pass generator expression. Loop variables don’t leak out.
  • Slicing s[start:stop:step] excludes stop, counts negatives from the end and never raises on out-of-range bounds: s[::-1] reverses, s[-3:] is the last three, s[:] copies.
  • Unpacking: a, b = b, a; first, *rest = xs; head, *_, tail = xs; f(*args, **kwargs); [*a, *b]; {**d1, **d2}.

Strings & f-strings

  • str is immutable Unicode; bytes is raw data (s.encode("utf-8")). Build with "".join(parts): += in a loop can go quadratic.
  • split() splits on whitespace runs and drops empties; split(",") keeps them. find returns -1, index raises ValueError.
name, n, pi = "Ada", 1234567, 3.14159
f"{name!r}"       # "'Ada'" (repr)
f"{n:,}"          # '1,234,567'
f"{pi:.2f}"       # '3.14'
f"{name:>6}|"     # '   Ada|'
f"{5:08b}"        # '00000101'
f"{0.25:.0%}"     # '25%'
f"{n=}"           # 'n=1234567' (3.8)
python
  • 3.14 t-strings (t"...") build a string.templatelib.Template, not a str, for safe escaping.

Functions & arguments

def f(a, b=2, /, c=3, *args, d, e=5, **kwargs): ...
# a, b positional-only (3.8) · c either · args extra positionals (tuple)
# d, e keyword-only (after *) · kwargs extra keywords (dict)
f(1, d=4)         # ok
f(a=1, d=4)       # TypeError: missing positional argument 'a'
python
  • Defaults are evaluated once, at def time, so a mutable default is shared by every call:
def append(item, bucket=[]):          # bug: append(1); append(2) gives [1, 2]
    bucket.append(item)
    return bucket
def append(item, bucket=None):        # fix
    bucket = [] if bucket is None else bucket
    bucket.append(item)
    return bucket
python
  • No return means None. Sort with key=: sorted(users, key=lambda u: (-u.score, u.name)).

Scope, closures & decorators

  • Lookup order is LEGB: Local, Enclosing, Global (module), Built-in.
  • Assigning to a name anywhere in a function makes it local there, so reading it first raises UnboundLocalError. Rebind with global x (module) or nonlocal x (enclosing function).
  • Closures capture variables, not values (late binding):
fs = [lambda: i for i in range(3)]
[f() for f in fs]                     # [2, 2, 2]: i is read at call time
fs = [lambda i=i: i for i in range(3)]
[f() for f in fs]                     # [0, 1, 2]: the default binds now
python
  • A decorator takes a function and returns a replacement: @deco means f = deco(f); @a over @b means a(b(f)). functools.wraps keeps __name__ and __doc__; without it every function is named wrapper.
import functools
def logged(fn):                       # plain decorator: @logged
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        print("calling", fn.__name__)
        return fn(*args, **kwargs)
    return wrapper
python
def repeat(n):                        # with arguments: @repeat(3)
    def deco(fn):                     # the factory returns the decorator
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            return [fn(*args, **kwargs) for _ in range(n)]
        return wrapper
    return deco
python

Iterators, generators & context managers

  • An iterable has __iter__; an iterator also has __next__ and raises StopIteration when done. for calls iter() then next() repeatedly.
  • A function with yield returns a generator: lazy, O(1) memory, keeps its state between next() calls, consumable once.
def chunks(xs, size):
    for i in range(0, len(xs), size):
        yield xs[i:i + size]
list(chunks([1, 2, 3, 4, 5], 2))      # [[1, 2], [3, 4], [5]]
def flatten(nested):
    for x in nested:
        if isinstance(x, list): yield from flatten(x)   # delegate
        else: yield x
list(flatten([1, [2, [3]]]))          # [1, 2, 3]
python
  • with calls __enter__, then __exit__(exc_type, exc, tb) even if the body raises; a truthy return from __exit__ suppresses the exception.
from contextlib import contextmanager
import time
@contextmanager
def timer(label):
    start = time.perf_counter()       # __enter__ part
    try:
        yield start                   # value bound by `as`
    finally:                          # __exit__ part, runs even on error
        print(f"{label}: {time.perf_counter() - start:.3f}s")
python
  • contextlib also has suppress(FileNotFoundError) and ExitStack.

Classes & OOP

Kind Receives Use for
instance method self behavior using instance state
@classmethod cls alternate constructors (cls is the subclass when inherited)
@staticmethod nothing a helper that belongs with the class
@property self computed or validated attribute; @x.setter for writes
  • The MRO (C3 linearization, Cls.__mro__) orders lookup: a class before its parents, parents in the order listed. super() calls the next class in the MRO, not necessarily the parent:
class A:
    def hi(self): return "A"
class B(A):
    def hi(self): return "B" + super().hi()
class C(A):
    def hi(self): return "C" + super().hi()
class D(B, C):
    def hi(self): return "D" + super().hi()
D().hi()          # 'DBCA'; D.__mro__ is D, B, C, A, object
python
  • No true privacy: __x is name-mangled to _Cls__x; _x is a convention.
  • abc.ABC + @abstractmethod: instantiating a class with unimplemented abstract methods raises TypeError. typing.Protocol gives structural (duck) typing.
  • Classes are instances of type. isinstance(x, A) respects inheritance; type(x) is A does not.

Dunders, dataclasses & slots

Method Triggered by Remember
__new__ / __init__ Cls(...) __new__ creates (immutables, singletons), __init__ initializes
__repr__ / __str__ repr(), print repr for developers; str falls back to __repr__
__eq__ / __hash__ ==, dicts, sets defining only __eq__ sets __hash__ to None
__lt__ <, sorted @functools.total_ordering fills in the rest
__bool__ / __len__ if x, len() truthiness tries __bool__, then __len__
__getitem__, __contains__, __iter__ x[k], in, for container protocol
__add__ / __radd__ a + b, b + a return NotImplemented for unknown types
__call__ obj() callable instances
__getattr__ missing attribute __getattribute__ runs on every access
from dataclasses import dataclass, field
@dataclass(order=True, frozen=True)
class Version:
    major: int
    minor: int = 0
    tags: list[str] = field(default_factory=list, compare=False)
Version(1, 2) < Version(1, 10)        # True: compares (major, minor)
Version(1) == Version(1, 0)           # True
Version(1).major = 2                  # FrozenInstanceError
python
  • @dataclass writes __init__, __repr__, __eq__; slots=True and kw_only=True came in 3.10. A bare [] default raises ValueError: use default_factory.
  • __slots__ = ("x", "y") drops the per-instance __dict__: less memory, and unlisted attributes raise AttributeError.

Exceptions

try:
    value = int(raw)
except (ValueError, TypeError) as e:          # parens optional without `as` (3.14)
    raise ConfigError(f"bad value {raw!r}") from e   # sets __cause__
else:
    print("parsed", value)                    # only if nothing was raised
finally:
    print("cleanup")                          # always, even after return
python
  • KeyboardInterrupt and SystemExit derive from BaseException, not Exception, so a bare except: swallows Ctrl-C. Custom exceptions subclass Exception (one base per library).
  • Order except clauses specific first. raise alone re-raises; raise X from None hides the original.
  • 3.11: ExceptionGroup, except*, e.add_note(). 3.14 warns when return, break or continue exits finally (it discards the exception).
  • EAFP (try/except) is idiomatic Python; LBYL (if k in d) is the alternative.

Standard library toolkit

collections Use
Counter(xs) frequencies; .most_common(k)
defaultdict(list) grouping; missing keys auto-created
deque(maxlen=n) O(1) at both ends; BFS, last-n buffers
OrderedDict move_to_end, popitem(last=False): LRU cache
namedtuple("P", "x y") lightweight immutable records
itertools Gives
chain.from_iterable(xss), islice(it, n) flatten one level, take n
groupby(xs, key) runs of consecutive equal keys: sort first
product, permutations, combinations Cartesian product, orderings, subsets
accumulate, pairwise (3.10), batched (3.12) prefix sums, neighbors, chunks
functools Use
cache (3.9), lru_cache(maxsize=128) memoization; hashable arguments only
partial, reduce pre-fill arguments, fold to one value
cached_property, total_ordering, singledispatch lazy attribute, comparisons, dispatch on type
  • Also: heapq (min-heap on a list), bisect (binary search on sorted lists).

Type hints

from typing import Protocol, TypedDict, Literal
def first[T](xs: list[T]) -> T | None:   # generics syntax (3.12)
    return xs[0] if xs else None
type Json = dict[str, Json] | list[Json] | str | int | float | bool | None
class Closer(Protocol):                  # any class with close() matches
    def close(self) -> None: ...
class User(TypedDict):
    id: int
    role: Literal["admin", "user"]
python
  • Hints are not enforced at runtime; mypy, pyright and IDEs check them. list[int] (3.9), int | None (3.10).
  • Optional[X] means X | None, not “optional argument”. Any disables checking. Also Callable, Final, Self (3.11).
  • 3.14 evaluates annotations lazily (PEP 649): forward references need no quotes.

Concurrency & asyncio

threading multiprocessing asyncio
Unit OS thread OS process task on one thread
CPU parallelism no (GIL) yes no
Best for blocking I/O CPU-bound work many concurrent network calls
Switching preemptive preemptive (OS) cooperative, at await
Sharing data shared memory + locks pickled via Queue/Pipe shared, single thread
  • The GIL lets one thread at a time run Python bytecode in standard CPython. Blocking I/O and many C extensions release it, so threads still help I/O-bound code.
  • It doesn’t make code thread-safe: count += 1 is several bytecodes. Use threading.Lock or queue.Queue.
  • Pools: concurrent.futures.ThreadPoolExecutor / ProcessPoolExecutor. In 3.14 Linux’s default start method became forkserver (was fork); macOS and Windows use spawn, so guard with if __name__ == "__main__":.

Note

Free-threaded CPython (PEP 703) drops the GIL: experimental in 3.13, officially supported in 3.14 as an optional separate build (often python3.14t; check sys._is_gil_enabled()). Single-threaded code runs somewhat slower, and some C extensions don’t support it yet.

import asyncio
async def fetch(i):
    await asyncio.sleep(0.1)                      # yields to the event loop
    return i * 2
async def main():
    async with asyncio.TaskGroup() as tg:         # 3.11; cancels siblings on error
        tasks = [tg.create_task(fetch(i)) for i in range(3)]
    print([t.result() for t in tasks])            # [0, 2, 4] in ~0.1 s, not 0.3
    print(await asyncio.gather(fetch(5), fetch(6)))   # [10, 12]
asyncio.run(main())
python
  • Calling an async def function only creates a coroutine; it runs when awaited or made a task.
  • Never block the loop with time.sleep, sync HTTP or heavy CPU: use await asyncio.sleep, async libraries, or asyncio.to_thread(fn). Timeouts: async with asyncio.timeout(5): (3.11).

Modules, packaging & pytest

  • A module is a .py file; a package is a directory of them (__init__.py optional for namespace packages). Top-level code runs once, on first import; then sys.modules caches it.
  • if __name__ == "__main__": runs only when the file is executed, not imported.
  • Imports search sys.path (script directory, PYTHONPATH, stdlib, site-packages). Fix circular imports by moving shared code or importing inside functions.
python3 -m venv .venv && source .venv/bin/activate
python -m pip install -r requirements.txt       # pip of THIS interpreter
python -m pip freeze > requirements.txt         # pin exact versions
python -m pytest -x -k login                    # stop at first failure, filter
Terminal
  • pyproject.toml holds project metadata, dependencies and tool settings.
import pytest
@pytest.fixture
def numbers():                          # `yield` instead of return adds teardown
    return [1, 2, 3]
def test_sum(numbers):                  # fixture injected by parameter name
    assert sum(numbers) == 6            # plain assert, detailed diff on failure
@pytest.mark.parametrize("a, b, total", [(1, 2, 3), (-1, 1, 0)])
def test_add(a, b, total):
    assert a + b == total
def test_div_zero():
    with pytest.raises(ZeroDivisionError, match="division by zero"):
        1 / 0
python
  • pytest collects test_*.py or *_test.py files and test-prefixed functions; share fixtures via conftest.py. Built-ins: tmp_path, monkeypatch, capsys.
  • unittest.mock.patch("app.service.requests"): patch where the name is looked up, not where it’s defined.

Quick answers

  • List vs tuple? Mutable vs immutable; a tuple of hashables can be a dict key.
  • Pass by value or reference? Call by sharing: the function gets a reference to the same object.
  • What is the GIL? A CPython lock letting one thread run bytecode at a time: threads for I/O, processes for CPU.
  • Generator vs list? Lazy, single-pass, O(1) memory vs materialized, reusable, indexable.
  • *args / **kwargs? Extra positional arguments as a tuple, extra keyword arguments as a dict.
  • What is a decorator? A callable wrapping a function to add behavior; use functools.wraps.
  • @staticmethod vs @classmethod? No implicit argument vs receives cls.
  • __str__ vs __repr__? Readable for users vs unambiguous for developers.
  • __new__ vs __init__? Creates the instance vs initializes it.
  • Shallow vs deep copy? New container sharing children vs a fully independent copy.
  • How is memory managed? Reference counting, plus a cyclic GC for reference cycles.
  • Why must dict keys be hashable? The hash locates the slot, so it must never change.
  • Threads, processes or asyncio? Blocking I/O, CPU-bound work, many sockets.

Gotchas & traps

  • Mutable defaults, late-binding closures and [[0] * n] * m all share one object (see above).
  • Resizing a dict or set while iterating raises RuntimeError; for lists, iterate a copy: for x in xs[:].
  • -7 // 2 is -4 (floors) and -7 % 2 is 1, but int(-3.5) is -3.
  • round(2.5) is 2 (round half to even). 0.1 + 0.2 == 0.3 is False: use math.isclose or Decimal.
  • True + True is 2, and {1: "a", True: "b"} is {1: 'b'}.
  • t = ([1],); t[0] += [2] raises TypeError and still mutates the list.
  • xs = xs.sort() sets xs to None. A second list(gen) is [].
  • except Exception as e: deletes e after the block.
  • x = float("nan"): x == x is False, yet [x] == [x] is True (identity is checked first).

Interview tip

Asked to optimize? Name the data structure: set/dict for O(1) lookups, deque for queues, heapq for top-k, Counter for frequencies, a generator for big streams.

Practice next: Python questions and the Python quiz.

esc