What Is the Python GIL? Global Interpreter Lock Explained
Concurrency Concepts
Demystifying the GIL
The GIL lets only one thread execute Python bytecode at a time. Here's what that actually means for your code — and what to do about it.
What the GIL actually locks
The Global Interpreter Lock is a mutex inside CPython that allows only one thread to execute Python bytecode at any moment. Ten threads can exist and hold live stack frames, but only the thread holding the GIL advances Python code; the interpreter forces a handoff every few milliseconds (tunable via sys.setswitchinterval), so threads interleave rapidly rather than run simultaneously.
The reason is memory management. CPython uses reference counting — every object carries a counter that concurrent threads would corrupt. One big lock around the interpreter makes every refcount operation safe, and keeps single-threaded code (most Python ever run) faster than fine-grained locking would.
The part that changes everything: the GIL is released whenever a thread isn't executing Python bytecode.
- Blocking I/O — network calls, file reads,
time.sleep, database queries — releases it, so other threads run while one waits. - Heavy C-extension work — NumPy linear algebra, hashing, compression — releases it around the C loops.
So the practical rule: threads give you parallel waiting, not parallel computing.
See it in numbers
CPU-bound work — threads buy nothing:
import time
from concurrent.futures import ThreadPoolExecutor
def crunch(_=None):
return sum(i * i for i in range(10_000_000))
start = time.perf_counter()
crunch(); crunch()
print(f"serial: {time.perf_counter() - start:.1f}s") # ~1.4s
start = time.perf_counter()
with ThreadPoolExecutor() as pool:
list(pool.map(crunch, range(2)))
print(f"threads: {time.perf_counter() - start:.1f}s") # ~1.4s — no speedup
I/O-bound work — threads shine, because the GIL is free while each thread waits on the network:
from concurrent.futures import ThreadPoolExecutor
import urllib.request
def fetch(url):
return urllib.request.urlopen(url).read()
urls = ["https://example.com"] * 20
# serial: ~20 × latency; threaded: ~1 × latency
with ThreadPoolExecutor(max_workers=20) as pool:
pages = list(pool.map(fetch, urls))
Getting real parallelism
| Workload | Reach for | Why |
|---|---|---|
| Many network calls / file ops | threading or asyncio | GIL released while waiting |
| Thousands of connections | asyncio | One thread, event loop multiplexes |
| CPU-heavy pure Python | multiprocessing / ProcessPoolExecutor | Each process has its own GIL |
| CPU-heavy numeric code | NumPy / Cython / Numba | C code releases the GIL in-process |
The one-line upgrade when compute is the bottleneck — same API, real cores:
from concurrent.futures import ProcessPoolExecutor # was ThreadPoolExecutor
with ProcessPoolExecutor() as pool:
results = list(pool.map(crunch, range(8)))
Caveats: processes don't share memory, arguments are pickled across the boundary, and the entry point needs an if __name__ == '__main__': guard.
Free-threaded Python (PEP 703)
CPython now ships an official free-threaded build — the experimental python3.13t / python3.14t interpreters — that removes the GIL entirely, letting threads run Python bytecode on multiple cores at once. The trade-offs today: some single-threaded overhead, and the ecosystem is still catching up (C extensions must ship free-threaded wheels). The default CPython from python.org and OS packages still has the GIL, so for code you deploy now, the model above is the one that applies.
Frequently Asked Questions
Does the GIL make Python threads useless?
No — it makes them specialized. For I/O-bound work (APIs, scraping, file processing), threads deliver near-linear speedups because the GIL is released during every blocking call. It is only CPU-bound pure-Python code where threads add nothing; that is what multiprocessing and C extensions are for.
Do NumPy and other C extensions bypass the GIL?
Largely, yes. Well-written extensions release the GIL around long C computations, so a NumPy matrix multiply can use multiple cores (via its BLAS backend) while other Python threads keep running. This is why numeric workloads often parallelize fine inside a single process.
Is the GIL being removed?
PEP 703 made a GIL-free build of CPython official starting with 3.13, as an explicitly experimental, separately-compiled interpreter. It becomes practical for production as libraries publish free-threaded wheels. The default build keeps the GIL for now — plan around it, and treat free-threading as something to watch, not something to depend on yet.