A strong answer
In standard CPython, ob_refcnt++ and ob_refcnt-- are plain non-atomic C integer operations (obj->ob_refcnt++). To prevent two OS threads from racing on ob_refcnt or pymalloc's global pools, CPython requires a thread to hold the Global Interpreter Lock (GIL) before executing Python bytecode. A waiting thread sleeps on a condition variable for sys.getswitchinterval() (5 ms); if the holder hasn't released the GIL (e.g., during blocking I/O or inside a C/Rust extension wrapped in Py_BEGIN_ALLOW_THREADS), the waiting thread sets the atomic eval_breaker bit, forcing the holder to drop the GIL at its next bytecode branch! In PEP 703 (Free-Threaded Python, python3.13t / 3.14t), the GIL is removed via three breakthroughs: (1) Immortal Objects (PEP 683): Singletons (None, True, small ints, interned strings) have a special high bit set in ob_refcnt so threads never modify their refcount (zero cache-line bouncing!). (2) Biased Reference Counting: Each object splits its refcount into ob_ref_local (modified with fast non-atomic instructions by the owning thread that created the object!) and ob_ref_shared (modified with LOCK XADD atomics only by other threads). (3) Per-Object Locks (PyMutex) and thread-safe mimalloc heaps.
Under standard GIL CPython, at most 1 OS thread executes Python bytecode at any instant (while arbitrarily many threads may execute C code inside Py_BEGIN_ALLOW_THREADS); under PEP 703 Biased Reference Counting, the owning thread modifies ob_ref_local non-atomically while non-owning threads modify ob_ref_shared atomically. Because only the single owning thread matching obj->ob_tid is permitted to read/write ob_ref_local during normal execution and all other threads use atomic compare-and-swap on ob_ref_shared, no data race can occur on reference counts even without a global lock.
Replacing every Py_INCREF with an atomic LOCK XADD across threads would slow single-threaded Python down by ~25% due to CPU cache-line bouncing; Immortalization + Biased Reference Counting keeps single-threaded overhead to just ~5–8% while unlocking 5x–7x multi-core scaling on 8 threads! In production systems (CPython Python/ceval_gil.c, Python 3.13t/3.14t (--disable-gil), NumPy/PyTorch Py_BEGIN_ALLOW_THREADS C kernels, and PyMutex 1-byte parking locks (Include/internal/pycore_lock.h).), engineers monitor these exact trade-offs to keep p99 latency predictable and prevent resource exhaustion under peak load.
What the interviewer is listening for
- Traces how the Global Interpreter Lock (GIL) and PEP 703 Biased Reference Counting operates in memory step by step rather than giving a vague textbook definition.
- States the core correctness invariant and explains why edge cases do not violate it.
- Quantifies time, space, and hardware costs (Pure Python Loop on 8 Threads (With GIL), NumPy / C Extension on 8 Threads (
ALLOW_THREADS)) with concrete numbers. - Identifies real-world failure modes (Assuming the GIL makes Python application state (
self.balance += amt) thread-safe) and explains how production systems mitigate them.
Common mistakes
- Treating the Global Interpreter Lock (GIL) and PEP 703 Biased Reference Counting as a black box without explaining the underlying memory or state transitions.
- Ignoring worst-case tail latency or memory overhead (Spawning 16 CPU-bound pure-Python threads on a GIL-enabled interpreter).
- Forgetting boundary conditions and error recovery (Modifying a global singleton's refcount from 32 threads in a C extension).