The Global Interpreter Lock (GIL)
The Global Interpreter Lock, almost always shortened to "the GIL," is one of the most talked-about — and most misunderstood — details of CPython, the reference implementation of Python that almost everyone uses. Understanding it explains why Python's threading module doesn't behave the way threads do in languages like Java or C++, and why multiprocessing exists as a separate tool in the standard library.
What the GIL Actually Is
The GIL is a single mutex (a lock) inside the CPython interpreter that allows only one thread to execute Python bytecode at any given moment — even on a machine with many CPU cores, and even if your program has started several threads. A thread must acquire the GIL before it can run any Python-level instructions, and it releases the GIL periodically so other waiting threads get a turn.
This means that in a pure-Python multithreaded program, your threads are never truly executing Python code at the same instant. They're rapidly taking turns holding the GIL, which can look like parallelism from the outside but is really fast-switching concurrency on a single core, from the interpreter's point of view.
Why the GIL Exists
CPython manages memory using reference counting — every object keeps a count of how many references point to it, and once that count hits zero, the object is freed. If two threads could simultaneously increment and decrement the same object's reference count without coordination, those updates could race and corrupt the count, leading to memory being freed too early (a crash) or never freed at all (a leak).
The GIL sidesteps this entire class of bugs with one simple rule: only one thread touches Python objects — and their reference counts — at a time. It's a blunt instrument, but it made CPython's internals dramatically simpler to write and made single-threaded code fast, at the cost of limiting multi-core scaling for multithreaded Python code. This was a deliberate, decades-old trade-off, not an oversight.
Practical Impact: CPU-Bound vs I/O-Bound
The GIL's effect depends entirely on what your threads are actually doing. For CPU-bound work — tight loops doing arithmetic, parsing, or any heavy pure-Python computation — threading gives you no speedup at all, because the threads are constantly fighting over the same lock and only one can ever compute at a time.
import threading
import time
def count(n):
while n > 0:
n -= 1
start = time.perf_counter()
count(50_000_000)
print(f"single-threaded: {time.perf_counter() - start:.2f}s")
start = time.perf_counter()
t1 = threading.Thread(target=count, args=(25_000_000,))
t2 = threading.Thread(target=count, args=(25_000_000,))
t1.start(); t2.start()
t1.join(); t2.join()
print(f"two threads: {time.perf_counter() - start:.2f}s")single-threaded: 1.85s two threads: 1.87s
Splitting the work across two threads didn't help — it's essentially the same total work, serialized by the GIL, plus a bit of thread-switching overhead. This is the single biggest surprise for developers coming from languages with real multithreaded parallelism.
I/O-bound work is a completely different story. When a thread makes a blocking call — reading a file, waiting on a network socket, sleeping — CPython releases the GIL for the duration of that wait, because there's no Python bytecode to protect while the thread is stuck in the operating system anyway. That means other threads are free to acquire the GIL and make progress while one thread is blocked on I/O, which is exactly why threading is genuinely useful for things like downloading many files concurrently, despite the GIL.
Workload type | Effect of Python threads | Why |
|---|---|---|
CPU-bound (math, parsing, tight loops) | No speedup — often slightly slower due to lock overhead | GIL forces only one thread to run Python bytecode at a time |
I/O-bound (network calls, file I/O, sleeping) | Real speedup — waits overlap | GIL is released during blocking syscalls and I/O waits |
Workarounds
multiprocessing — spins up separate OS processes, each with its own Python interpreter and its own GIL, giving you genuine parallel execution across CPU cores for CPU-bound work at the cost of process startup overhead and needing to serialize data passed between processes
C extensions that release the GIL — libraries like NumPy, pandas, and many others do their heavy lifting in compiled C code and explicitly release the GIL while that native code runs, so a NumPy matrix multiply can genuinely run in parallel with other Python threads
Alternative interpreters — implementations like PyPy use different execution strategies (PyPy still has a GIL but a much faster JIT-compiled interpreter loop), and some experimental interpreters remove the GIL model entirely; these come with their own compatibility trade-offs and are far less commonly deployed than CPython
Looking Ahead: Free-Threaded CPython
Summary
The GIL is a mutex in CPython that lets only one thread execute Python bytecode at a time
It exists mainly to keep reference counting (and other interpreter internals) safe without complex fine-grained locking
Threading doesn't speed up CPU-bound Python code, but does help I/O-bound code, since the GIL is released during blocking I/O
multiprocessing, GIL-releasing C extensions like NumPy, and alternative interpreters are the standard workarounds for CPU-bound parallelism
PEP 703's free-threaded CPython build, available experimentally since Python 3.13, is an active effort to make the GIL optional in the future