Every rule in this entire site โ "processes for CPU, threads for I/O", the pickling tax, the whole shape of multiprocessing โ is downstream of one design decision inside CPython. This is that decision.
The Global Interpreter Lock is a single mutex inside the CPython interpreter. To execute any Python bytecode, a thread must first acquire that mutex. Because there is exactly one GIL per interpreter, only one thread runs Python bytecode at any instant โ even on a 64-core machine with 64 threads ready to go.
What does it protect? Chiefly CPython's reference counts. Every object carries an integer counting how many references point at it; when that count hits zero, the object is freed. That counter is touched constantly โ on nearly every assignment, argument pass, and attribute access. If two threads incremented and decremented the same refcount without coordination, the counts would corrupt: objects would be freed while still in use (use-after-free crashes) or leak forever. The GIL makes each bytecode step's refcount bookkeeping safe by fiat, because only one thread is ever touching those counters.
It is tempting to read the GIL as pure legacy baggage, but it bought CPython three concrete things that shaped the whole ecosystem:
++/-- under one big lock. The
common case (a single thread) stays cheap. Fine-grained locking on every object would tax
every program to benefit the few that use threads for CPU.cryptography) grew so large and stayed so reliable.A running thread doesn't keep the GIL forever โ it releases it periodically so other runnable threads get a turn. Conceptually, threaded execution is a relay race where the baton is the GIL: everyone can be scheduled, but only the baton-holder actually runs Python bytecode.
The interpreter invites a switch using the switch interval (5 ms by default). After that interval elapses, the interpreter requests that the current thread drop the GIL at the next safe point. It is a fairness/latency knob, not a performance knob โ lowering it makes switching more responsive but adds hand-off overhead. It does not give you parallelism; it just shares one core more finely.
import sys
sys.getswitchinterval() # 0.005 โ 5 milliseconds (default)
sys.setswitchinterval(0.001) # ask for switches ~every 1ms (fairness, not speed)
Here is the sentence this entire site is built on. The GIL is per interpreter. And every OS process that runs Python gets its own private interpreter โ therefore its own private GIL. Two GILs cannot serialize each other, because neither knows the other exists.
multiprocessing; it simply sidesteps the GIL by
putting each worker behind its own copy of it.
So the framing for the rest of these guides: multiprocessing is the GIL workaround that ships in the standard library. It doesn't remove the GIL โ it gives every worker its own, and pays for that isolation with memory copies and pickling (see Pattern 8 and Processes 101).
The GIL only serializes threads while they run pure-Python bytecode. A thread releases the GIL whenever it is about to wait on something outside the interpreter, or when it enters C code that explicitly says it is safe to let others run. During that window, other Python threads make real progress. This is the entire reason threads help I/O-bound work at all โ and the dividing line for whether threads or processes are the right tool.
| Workload | Does the GIL block it? | Threads help? | Processes help? |
|---|---|---|---|
Pure-Python CPU (loops, parsing, arithmetic on int/float) | โ Held the whole time | โ No โ they serialize | โ Yes โ a GIL each, true parallelism |
| Blocking network / socket I/O | โ Released (parked in kernel) | โ Yes โ overlap the waiting | โ Works, but heavier than needed |
| Blocking disk / file I/O | โ Released (kernel wait) | โ Yes | โ Works, overkill |
time.sleep(n) | โ Released for the whole sleep | โ Yes | โ Works, overkill |
NumPy np.dot, hashlib, zlib, compiled C that releases the GIL | โ Released while in C | โ Yes โ real CPU overlap in C | โ Yes โ also fine |
| C extension touching Python objects (not releasing it) | โ Held | โ No | โ Yes |
Note the NumPy row is why "just use threads" sometimes works for numeric code: a big
np.dot releases the GIL and crunches in C (often across several BLAS threads) while
your other Python threads keep running. But the moment your hot loop is pure Python, that
escape hatch closes and you are back to needing processes.
Python 3.13 introduced an experimental free-threaded build โ a separate
interpreter compiled with --disable-gil, shipped as python3.13t (the
trailing t = "threaded"). Python 3.14 continues stabilizing it. In this build the GIL
is genuinely gone: threads run Python bytecode in true parallel across cores, the
way threads do in Java or Go.
To detect which build you are on:
import sys
# True on a normal (GIL) build; False on a free-threaded build (3.13+).
sys._is_gil_enabled()
python.joblib, Dask, Ray โ see
Common Libraries) is built on the process model.There is a third point on the spectrum, new and still evolving. Sub-interpreters let you run multiple interpreters inside one process, and since Python 3.12 each sub-interpreter has its own GIL. So they run bytecode in parallel โ like processes โ but without spinning up a whole new OS process.
Python 3.12 exposed sub-interpreters at the C-API level; a friendlier standard-library Python API
(interpreters) is arriving through 3.13/3.14. Treat this as new and
evolving โ promising for CPU parallelism with lower overhead than processes, but not yet
the boring, battle-tested default that multiprocessing is.
| Concept | Takeaway |
|---|---|
| What the GIL is | One CPython mutex; only one thread runs bytecode at a time |
| What it protects | Interpreter state, chiefly non-atomic reference counts |
| Why it exists | Fast simple refcounting + easy, fast C extensions |
| The pivotal fact | One GIL per interpreter; one interpreter per process โ a GIL per process |
| Why processes parallelize | N processes = N GILs = true N-core parallelism |
| Released during | I/O, time.sleep, GIL-releasing C (NumPy, hashlib, zlib) |
| Free-threaded build (PEP 703) | 3.13+ python3.13t: no GIL, experimental, ecosystem catching up |
| Sub-interpreters (PEP 734) | 3.12+ many interpreters/process, a GIL each โ a lighter middle ground, still evolving |