What experienced Python engineers forget before an interview, grouped by subtopic.
- CPython compiles to bytecode run by a stack-based VM; the GIL lets only one thread execute bytecode at a time, keeping refcounting safe without per-object locks.
- The GIL is released during blocking I/O and by some C extensions, so threads still help I/O-bound work; CPU-bound work needs processes or a GIL-releasing extension.
- A running thread is asked to drop the GIL every 5 ms (
sys.getswitchinterval()), which is why CPU-bound threads also slow I/O threads. - The GIL doesn't make statements atomic —
x += 1on a shared value is several bytecodes and still races; use aLock.
- The free-threaded build (no GIL) was experimental in 3.13 and is officially supported since 3.14 (PEP 779); an incompatible C extension can force the GIL back on.
concurrent.interpreters(3.14, PEP 734) runs several interpreters in one process, each with its own GIL, talking over cross-interpreter queues.
- 3.11 added the adaptive specializing interpreter, roughly 25% faster on average.
- Hot bytecodes are rewritten in place into type-specialized versions, which fall back when the observed types change.
- Reference counting frees an object the instant its count hits zero (the free-threaded build can delay it).
- A generational cyclic GC periodically finds and frees reference cycles, which counting alone can't.
- CPython caches small ints (-5 to 256) and interns some strings, so
iscan returnTruefor equal values by accident. - Use
==for values andisonly for singletons likeNone.
__slots__replaces the per-instance__dict__with fixed storage — less memory per instance, no arbitrary new attributes.weakrefholds a reference that doesn't keep the object alive — for caches and observers.__del__runs at refcount zero, or only at the next GC pass if the object is in a cycle (collectable since 3.4, PEP 442); usewithfor deterministic cleanup (weakref.finalizeis a safer__del__, not more deterministic).
| Operation | list | dict / set | deque |
|---|---|---|---|
| index / lookup | O(1) | O(1) avg | O(n) |
| append / add | O(1) amortized | O(1) avg | O(1) both ends |
| insert/pop at front | O(n) | — | O(1) |
x in c |
O(n) | O(1) avg | O(n) |
list.sort()/sortedis Timsort: stable, O(n log n), O(n) on nearly sorted data.heapqis a min-heap over a plain list; max-heap functions (heappush_max,heappop_max) exist since 3.14; before that, push negated keys.
- A dict is an open-addressing hash table plus a compact, insertion-ordered entries array — that layout is why order is preserved.
- Keys must be hashable; a mutable object whose hash changes after insertion becomes unfindable.
| Model | Best for | Cost |
|---|---|---|
| threads | I/O-bound, blocking libraries | GIL limits CPU parallelism |
| processes | CPU-bound | memory, pickling/IPC overhead |
| asyncio | many concurrent I/O waits | everything must be non-blocking |
forkcopies the parent — fast, but can inherit inconsistent state (a lock held by another thread mid-acquire).spawnstarts a fresh interpreter — slower, safest;forkserverforks children from a clean single-threaded server process.forkserveris the Linux default since 3.14 (previouslyfork); macOS defaults tospawnsince 3.8, Windows always has.- With
spawn/forkserver, children re-import the main module, so guard entry code withif __name__ == "__main__":; the target, arguments, and results must pickle (no lambdas, local functions, or open file objects).
- One thread runs the loop: it polls ready I/O (epoll/kqueue via
selectorson Unix; IOCP proactor on Windows since 3.8), then runs ready callbacks; a coroutine only yields control at anawaiton something not yet ready. asyncio.create_taskschedules a coroutine; calling a coroutine function without awaiting or scheduling it does nothing.- Keep a reference to created tasks — the loop holds only weak references, so an unreferenced task can be garbage-collected mid-run.
TaskGroup(3.11) is structured concurrency: one failure cancels the siblings and waits for them;gatherdoesn't cancel the others by default.- A blocking call in a coroutine (
time.sleep, sync file I/O) stalls the whole event loop — use async libraries orasyncio.to_thread. - Cancellation raises
CancelledErrorat the nextawait;finallycleanup propagates it automatically, but anexceptthat catches it must re-raise.
- Defaults are evaluated once, at definition time —
def f(x, cache=[])shares one list across calls; default toNoneand create the object inside. - Dataclasses reject unhashable defaults (
list,dict,set) — usefield(default_factory=list). - A closure captures the variable, not its value —
[lambda: i for i in range(3)]all return2; a default binds early:lambda i=i: i.
- LEGB lookup: Local, Enclosing, Global, Built-in.
- Assigning to a name anywhere in a function makes it local for the whole body, so reading it before the assignment raises
UnboundLocalError; usenonlocal/global.
//and%floor toward −∞:-7 // 2 == -4and-7 % 2 == 1; C, Go, Rust, and JavaScript truncate toward zero (-7 % 2 == -1).round()rounds half to even:round(2.5) == 2,round(3.5) == 4;round(2.675, 2) == 2.67because the float is slightly below 2.675.intnever overflows (arbitrary precision);floatis an IEEE 754 double, so usedecimal.Decimalbuilt from strings for money.
- The default limit is 1000 frames (
sys.getrecursionlimit()); going deeper raisesRecursionError. - There's no tail-call optimization, so deep DFS or memoized recursion on large inputs needs an explicit stack.
sys.setrecursionlimithelps for moderate depths; for depths around 10⁵ and beyond, rewrite iteratively.
[[0] * 3] * 3builds three references to the same inner list — mutating one row mutates all.copy.copy()copies only the outer object;copy.deepcopy()recurses and handles cycles via a memo dict.
- Removing items from a list inside
forskips elements. - Resizing a dict while iterating raises
RuntimeError: dictionary changed size during iteration— iterate over a copy (dict views).
- Loaded modules are cached in
sys.modules, so top-level code runs once per process. - A circular import hands one module a partially initialized module missing the name it needs — import inside the function, break the cycle, or guard type-only imports with
TYPE_CHECKING.
- Multiple inheritance resolves via C3 linearization (the MRO,
Cls.__mro__). super()follows the instance's MRO, not the direct parent, so every class in a mixin chain must callsuper().__init__().
- A mutable class attribute (
items = []in the class body) is shared by every instance. self.items.append(x)mutates the shared list;self.items = [...]creates an instance attribute that shadows it.- Create per-instance state in
__init__; dataclasses forcefield(default_factory=list)for this reason.
- An object with
__get__/__set__/__delete__on a class attribute is a descriptor;property, bound methods,classmethod, andstaticmethodare all built on it. - Data descriptors (define
__set__or__delete__) beat the instance__dict__; non-data descriptors (only__get__, e.g. functions) lose to it.
__new__creates the instance,__init__only initializes it — override__new__for immutable subclasses or instance caching.- Metaclasses customize class creation;
__init_subclass__covers most "react when a subclass is defined" needs without one.
- Defining
__eq__without__hash__sets__hash__ = None, making instances unhashable. @dataclass(frozen=True)blocks field reassignment (shallow — a list field stays mutable) and generates__hash__, which raisesTypeErroron an unhashable field;slots=True(3.10) adds__slots__.
- Generators are lazy and exhausted after one pass;
yield fromdelegates to a sub-generator. gen.send(v)resumes a generator and makesvthe value of the pausedyieldexpression.
- Wrap with
functools.wraps(func)or the wrapper loses__name__,__doc__, and signature. - A decorator with arguments needs an extra layer:
@deco(arg)callsdeco(arg), which must return the real decorator.
@cache(3.9) islru_cache(maxsize=None)— unbounded, so it grows forever on unbounded inputs; switch tolru_cache(maxsize=N)to bound it.- On a method it keys on
selfand keeps every instance alive; usecached_propertyor a per-instance cache. - Arguments must be hashable, and a returned mutable object is shared by every caller.
__exit__returningTruesuppresses the exception;contextlib.contextmanagerturns a one-yieldgenerator into a context manager.try ... elseruns only when no exception was raised;except*(3.11) handles exception groups, e.g. from aTaskGroup.
- Type hints are not enforced at runtime — they're for
mypy/pyright. typing.Protocolgives structural typing: a class matches by having the methods, unlike ABCs, which need subclassing or registration.TypedDicttypes a dict's known keys without making it a class.
- PEP 695 syntax (
class Box[T]: ...,type Alias = ..., 3.12) replaces manualTypeVardeclarations with scoped type parameters. - Annotations are evaluated lazily since 3.14 (PEP 649), making
from __future__ import annotationslargely unnecessary.
match(3.10) destructures by shape:case {"type": "click", "x": x},case Point(x=0).- A bare name in a
casebinds, it doesn't compare —case RED:matches everything; use dotted names (Color.RED) for constants.
- t-strings (
t"Hello {name}", 3.14, PEP 750) produce aTemplateobject with static parts and interpolated values kept separate. - A library receiving it can escape values safely (SQL, HTML), unlike an f-string that arrives already joined.
pyproject.tomlis the standard manifest;uvresolves, installs, and manages Python versions far faster than pip, with a lock file.pytestfixture scope (function,class,module,session) sets how often it's rebuilt — asessionfixture holding mutable state leaks between tests.
cProfilegives function-level call counts and cumulative time;py-spysamples a running process without code changes, safe in production.tracemallocsnapshots allocations by line to find memory growth.