The Python GIL, Explained (and How to Work Around It)
The Global Interpreter Lock lets only one thread run Python bytecode at a time, so threads don't speed up CPU-bound work. Here's what the GIL is, why it exists, and how to get real parallelism with multiprocessing and async.
The Global Interpreter Lock (GIL) is a lock in CPython that lets only one thread execute Python bytecode at a time. The practical consequence: spinning up multiple threads will not speed up CPU-heavy Python work, because they can’t run Python code on different cores at the same time — they take turns.
This surprises people who expect threads to use all their cores, and it’s a staple of senior and staff interviews. The good news: once you know which kind of work the GIL limits, the workarounds are straightforward.
What the GIL actually does
CPython (the standard Python implementation) protects its internal state with a single global lock. To run any Python bytecode, a thread must hold the GIL. So even with eight threads on an eight-core machine, only one is running Python code at any instant; the others wait for the lock.
# This uses 4 threads but won't run 4× faster on CPU-bound work —
# they take turns holding the GIL.
from threading import Thread
def crunch(n):
total = 0
for i in range(n):
total += i * i
return total
threads = [Thread(target=crunch, args=(10_000_000,)) for _ in range(4)]
Why does Python have a GIL?
It’s a deliberate trade-off. The GIL makes CPython’s memory management (reference counting) simple and fast, makes single-threaded programs quick, and makes integrating C libraries easy. The cost is no free multi-core threading for pure-Python CPU work. For most of Python’s history, that trade has been worth it — most programs are I/O-bound, not CPU-bound.
The key distinction: CPU-bound vs I/O-bound
This is the whole interview answer.
| Workload | Bottleneck | Does the GIL hurt? | Use |
|---|---|---|---|
| CPU-bound | computation (math, parsing, image work) | Yes — threads take turns | multiprocessing |
| I/O-bound | waiting (network, disk, DB) | No — GIL released while waiting | threads or asyncio |
When a thread waits on I/O, it releases the GIL, so other threads run. That’s why threading still speeds up programs that spend their time waiting on the network — downloading 100 URLs with threads really is faster.
asyncio give you real concurrency, because the GIL is released while a thread waits.
Working around the GIL
1. Multiprocessing — true parallelism for CPU work. Separate processes each have their own interpreter and GIL, so they run on multiple cores:
from concurrent.futures import ProcessPoolExecutor
with ProcessPoolExecutor() as pool:
results = list(pool.map(crunch, [10_000_000] * 4)) # real parallelism
The cost is heavier memory and inter-process communication, so pass data in chunks, not constantly.
2. asyncio — massive I/O concurrency, single-threaded. For thousands of network calls, async/await handles them on one thread without thread overhead — conceptually the same event-loop model as JavaScript’s async.
3. Lean on C extensions. NumPy, pandas, and many libraries release the GIL during heavy C-level computation, so they already use multiple cores for the numeric parts.
4. The free-threaded future. Python 3.13 shipped an experimental no-GIL build (PEP 703). It’s not yet the default, so for now the workarounds above are how real systems scale.
Common misconceptions
- “The GIL makes Python slow.” No — it limits one specific pattern (CPU-bound threading). Single-threaded and I/O-bound code are unaffected.
- “Threads are useless in Python.” False — they’re great for I/O-bound concurrency.
- “Async gives you parallelism.” No —
asynciois concurrency on one thread (great for I/O), not multi-core parallelism. Use multiprocessing for CPU parallelism. - “Every Python has a GIL.” It’s a CPython implementation detail; Jython and the new free-threaded build differ.
threading to speed up a number-crunching loop is the classic mistake — the GIL means it won't get faster. Use multiprocessing (or a C-accelerated library) for CPU-bound parallelism.
Where this fits
The GIL is Phase 7 — internals and performance — in our Python roadmap, and it’s where concurrency, the memory model, and async thinking come together. It’s exactly the kind of “do you really understand the runtime?” question that separates job-ready from staff-level.
The GIL, multiprocessing, asyncio, and Python’s memory and concurrency model are worked through in depth in Python for Staff Engineers, with the threading-vs-multiprocessing decision and patterns introduced in Python in Three Months.
Know which work is CPU-bound and which is I/O-bound, and the GIL stops being a mystery and becomes a design decision.
Frequently asked questions
What is the GIL in Python?
The GIL (Global Interpreter Lock) is a mutex in CPython that allows only one thread to execute Python bytecode at a time. It means that even on a multi-core CPU, a multithreaded Python program cannot run Python code on more than one core simultaneously.
Does the GIL make Python slow?
Not for most programs. The GIL only limits CPU-bound work spread across threads. I/O-bound work (network, disk, database) is unaffected because the GIL is released while waiting. Single-threaded performance is not reduced by the GIL at all.
How do I get true parallelism in Python despite the GIL?
Use the multiprocessing module (or concurrent.futures.ProcessPoolExecutor) to run work in separate processes, each with its own GIL and interpreter, so they run on multiple cores. For I/O-bound concurrency, use threads or asyncio. Heavy numeric libraries like NumPy also release the GIL during C-level computation.
Is the GIL being removed from Python?
Python 3.13 introduced an experimental free-threaded build (PEP 703) that can run without the GIL, and work continues to make it production-ready. For now, most deployments still use the standard GIL build, so understanding the GIL and its workarounds remains essential.