Python Quick Reference

Quick reference for the patterns that come up most often in real code

Why

Knowing these patterns cold — join over repeated +=, dict.get over a bare lookup that raises KeyError — is the difference between fluent code and hesitant code. This page collects the patterns from every other Python topic in one scannable place for quick review.

Explanation
Strings
# step slicing — s[start:stop:step] s = "abcde" s[::-1] # "edcba" — reverse s[::2] # "ace" — every 2nd char s[4:1:-1] # "edc" — from idx 4 down to (not incl.) 1 # join is O(n) — prefer over += "".join(parts) # O(n), one allocation # s += word in loop # O(n²) — new object each time # find vs index s.find("x") # -1 if missing s.index("x") # raises ValueError if missing # f-string format specs f"{x:.2f}" # 2 decimal places f"{42:05d}" # "00042" — zero-padded f"{1_000_000:,}" # "1,000,000" — thousands sep f"{val!r}" # calls repr() on val # interview one-liners s == s[::-1] # palindrome len(s) == len(set(s)) # all unique chars Counter(s) # char frequency map Counter("listen") == Counter("silent") # anagram s.encode("utf-8") # str → bytes b"hello".decode("utf-8") # bytes → str
Data Structures & Collections
# starred unpacking a, *rest = (1, 2, 3, 4) # rest = [2, 3, 4] first, *mid, last = range(5) # set ops (O(1) membership) {1,2,3} & {2,3,4} # {2,3} intersection {1,2,3} | {2,3,4} # {1,2,3,4} union {1,2,3} - {2,3,4} # {1} difference s.discard(x) # no error if missing (s.remove raises KeyError) # dict d.get("key", default) for k, v in d.items(): ... sorted(d.items(), key=lambda i: i[1]) # sort by value d1 | d2 # merge (3.9+) from collections import Counter, defaultdict, deque, namedtuple from typing import NamedTuple Counter("abracadabra").most_common(2) # [("a",5),("b",2)] Counter("abc")["z"] # 0 — no KeyError graph = defaultdict(list) graph["A"].append("B") # no KeyError on first access q = deque(maxlen=3) # auto-evicts oldest when full q.appendleft(x) # O(1) — list.insert(0,x) is O(n) # namedtuple — immutable, tuple-compatible, named fields Point = namedtuple("Point", ["x", "y"]) p = Point(1, 2); p.x # 1 class Coord(NamedTuple): # typed + default support x: float; y: float = 0.0 # heapq — min-heap; always gives smallest first import heapq h = [3, 1, 4]; heapq.heapify(h) # O(n) in place heapq.heappush(h, 2); heapq.heappop(h) heapq.nsmallest(2, h) # top-k without full sort heapq.heappush(h, (-score, item)) # max-heap: negate the key # Enum — named constants, identity comparison from enum import Enum, auto class Status(Enum): PENDING = auto(); DONE = auto() Status.PENDING.name # "PENDING" Status.PENDING.value # 1 Status["PENDING"] # lookup by name for i, v in enumerate(items, start=1): ... for a, b in zip(xs, ys): ... # stops at shortest all(x > 0 for x in vals) # short-circuits on False any(x > 0 for x in vals) # short-circuits on True
Functions & Scope — traps
# mutable default — evaluated ONCE at definition def bad(lst=[]): # same list every call ← bug lst.append(1); return lst def good(lst=None): # correct pattern if lst is None: lst = [] lst.append(1); return lst # unpack at call site vals = [1, 2]; opts = {"z": 3} f(*vals, **opts) # f(1, 2, z=3) # late-binding closure trap fns = [lambda: i for i in range(3)] [f() for f in fns] # [2,2,2] — i looked up at CALL time # fix — capture value as default arg (evaluated at def time) fns = [lambda i=i: i for i in range(3)] [f() for f in fns] # [0,1,2] # nonlocal — mutate enclosing scope variable def counter(): n = 0 def inc(): nonlocal n; n += 1; return n return inc
Error Handling
try: result = risky() except (TypeError, ValueError) as e: handle(e) else: success() # runs ONLY if no exception was raised — often forgotten finally: cleanup() # always runs — even if there's a return or raise inside try raise ValueError("wrapped") from original_err # chains — preserves traceback raise # re-raise current exception class AppError(Exception): def __init__(self, msg, code): super().__init__(msg) self.code = code
Comprehensions & Generators
# dict / set comp — often forgotten {k: v for k, v in d.items() if v} {x % 3 for x in range(10)} # nested — reads left-to-right like nested for loops [x*y for x in rows for y in cols] # conditional expression inside comp [x if x > 0 else -x for x in nums] # inline abs() # generator — lazy, O(1) memory gen = (x**2 for x in range(10**6)) next(gen) # pull one value at a time def stream(n): while n > 0: yield n; n -= 1 # pauses here each call # yield from — delegate to another iterable/generator def flatten(lst): for item in lst: if isinstance(item, list): yield from flatten(item) # instead of: for x in flatten(item): yield x else: yield item # key difference sum(x**2 for x in range(10**6)) # O(1) memory sum([x**2 for x in range(10**6)]) # O(n) — list built first
Sorting & Functional Tools
from operator import itemgetter, attrgetter sorted(people, key=lambda p: p[1]) sorted(people, key=itemgetter(1)) # faster than lambda sorted(objs, key=attrgetter("salary")) sorted(p, key=lambda p: (p[1], p[0])) # multi-key tuple list(map(str, nums)) # use named fn, not lambda list(filter(None, vals)) # drops all falsy values from functools import reduce from operator import add reduce(add, [1,2,3,4], 0) # initialiser avoids empty-list error import itertools list(itertools.chain([1,2],[3,4])) # [1,2,3,4] list(itertools.combinations([1,2,3], 2)) # [(1,2),(1,3),(2,3)] list(itertools.product([0,1], repeat=3)) # all 3-bit combos # groupby — MUST sort by same key first data.sort(key=itemgetter("dept")) for dept, grp in itertools.groupby(data, key=itemgetter("dept")): print(dept, list(grp))
Decorators
import functools def log(func): @functools.wraps(func) # preserve __name__ def wrapper(*args, **kwargs): print(f"→ {func.__name__}") result = func(*args, **kwargs) print(f"← done") return result return wrapper @log def process(data): ... # decorator with arguments — 3 levels def retry(times=3): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): for i in range(times): try: return func(*args, **kwargs) except Exception: if i == times-1: raise return wrapper return decorator from functools import cache @cache # memoize — args must be hashable def fib(n): return n if n < 2 else fib(n-1)+fib(n-2)
OOP
class Dog(Animal): def __init__(self, name, breed): super().__init__(name) # always call super().__init__ self.breed = breed def speak(self): return f"{super().speak()}!" isinstance(d, Animal) # True — respects inheritance issubclass(Dog, Animal) # True type(d) is Dog # exact type only — avoid in favour of isinstance class C: shared = [] # shared across ALL instances ← trap def __init__(self): self.own = [] # unique per instance class Circle: def __init__(self, r): self._r = r @property def radius(self): return self._r # c.radius — no () @radius.setter def radius(self, v): if v < 0: raise ValueError self._r = v @classmethod # gets cls, not self def from_diameter(cls, d): return cls(d/2) @staticmethod # no cls or self def is_valid(r): return r >= 0 from dataclasses import dataclass, field @dataclass class Pipeline: name: str steps: list = field(default_factory=list) # mutable default @dataclass(frozen=True) # immutable + auto __hash__ class Point: x: float; y: float
Dunder Methods
class Vector: def __init__(self, x, y): self.x, self.y = x, y def __repr__(self): return f"Vector({self.x}, {self.y})" # REPL / logs def __str__(self): return f"({self.x}, {self.y})" # print() def __add__(self, o): return Vector(self.x+o.x, self.y+o.y) # v1 + v2 def __eq__(self, o): return (self.x, self.y) == (o.x, o.y) # v1 == v2 def __hash__(self): return hash((self.x, self.y)) # MUST pair with __eq__ def __len__(self): return 2 # len(v) def __bool__(self): return self.x != 0 or self.y != 0 # bool(v) def __iter__(self): yield self.x; yield self.y # for val in v def __call__(self, scale): return Vector(self.x*scale, self.y*scale) # v(2) def __contains__(self, v): return v in (self.x, self.y) # x in v

Defining __eq__ without __hash__ → Python sets __hash__ = None — instances become unhashable. Always define both.

Context Managers
class Timer: def __enter__(self): self.start = time.time(); return self def __exit__(self, exc_type, exc, tb): self.elapsed = time.time() - self.start return False # False = don't suppress, True = swallow exception with Timer() as t: do_work() print(t.elapsed) from contextlib import contextmanager, suppress @contextmanager def managed(): resource = acquire() try: yield resource # "as" value finally: release(resource) # always runs with suppress(FileNotFoundError): os.remove("tmp.txt")
Type Hints
# modern syntax (3.10+) def f(x: int | None) -> list[int]: ... from typing import Optional, Union, TypeVar, Protocol, Callable def f(x: Optional[int]) -> Union[str, int]: ... # Optional[X] == X | None T = TypeVar("T") def first(items: list[T]) -> T: return items[0] class Drawable(Protocol): # structural ("duck") typing def draw(self) -> None: ... def apply(fn: Callable[[int], str], x: int) -> str: return fn(x)
Concurrency & Async
# GIL rule: # threading → I/O-bound (network, disk, sleep) # multiprocessing → CPU-bound (compute, parsing) from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor with ThreadPoolExecutor(max_workers=4) as ex: results = list(ex.map(fn, items)) # I/O with ProcessPoolExecutor() as ex: results = list(ex.map(fn, items)) # CPU import asyncio async def main(): results = await asyncio.gather( # concurrent, not sequential fetch("url1"), fetch("url2"), fetch("url3"), ) asyncio.run(main()) async with aiohttp.ClientSession() as s: async with s.get(url) as resp: data = await resp.json()