Knowing the mechanism lets you predict performance and avoid relying on accidental thread-safety.
The GIL is a mutex around the interpreter's internal state. It exists mainly because CPython's memory management uses reference counting: every object's count changes constantly, and the GIL makes those updates safe without a lock per object.
How threads share it:
- A running thread holds the GIL and executes bytecode.
- A waiting thread sets a "drop request" after the switch interval (
sys.getswitchinterval(), default 5 ms). The running thread checks at safe points between bytecodes and hands the GIL over.sys.setswitchinterval()tunes this trade-off between responsiveness and throughput. - Blocking calls release it explicitly: C code wraps slow work in
Py_BEGIN_ALLOW_THREADS/Py_END_ALLOW_THREADS. That's whytime.sleep, socket I/O,hashlibon large data,zliband NumPy kernels run in parallel.
What the GIL protects: interpreter internals and the atomicity of individual bytecodes and many built-in C operations. What it doesn't: your invariants. Switches can happen between any two bytecodes, so multi-step logic needs locks.
Consequences: CPU-bound threads add switching overhead, and one CPU-heavy thread can delay an I/O thread's response by up to the switch interval each time it wants the GIL back. Python 3.12 added per-interpreter GILs (PEP 684), and 3.13 an optional build without a GIL.
Example
import hashlib, sys, threading, time
print(sys.getswitchinterval()) # 0.005
data = b"x" * 50_000_000
def digest():
hashlib.sha256(data).hexdigest() # C code releases the GIL for large inputs
def py_loop():
sum(range(10_000_000)) # bytecode: holds the GIL
def timed(fn, n):
t = time.perf_counter()
threads = [threading.Thread(target=fn) for _ in range(n)]
for th in threads: th.start()
for th in threads: th.join()
return time.perf_counter() - t
for fn in (digest, py_loop):
print(fn.__name__, "4 threads vs 1:", round(timed(fn, 4) / timed(fn, 1), 1), "x")
# on a machine with 4+ free cores:
# digest 4 threads vs 1: ~1 x -> runs in parallel (GIL released)
# py_loop 4 threads vs 1: ~4 x -> serialised by the GILRemember
- The GIL protects reference counts and interpreter state
- Threads hand it over every switch interval (5 ms) or when blocking
- C extensions release it around heavy work, enabling real parallelism
Watch out for
- Treating 'the GIL makes it safe' as a design principle; it doesn't protect multi-step invariants.