PythonThe Global Interpreter Lock (GIL)

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.

Python
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

Tip
If your bottleneck is genuinely CPU-bound pure-Python computation, reach for multiprocessing (or push the hot loop into a GIL-releasing C extension like NumPy) rather than threading. If your bottleneck is waiting on the network, disk, or other I/O, threading — or asyncio — is the right tool, GIL or not.
Looking Ahead: Free-Threaded CPython
Note
PEP 703 proposed making the GIL optional in CPython, and an experimental "free-threaded" build has been available starting with Python 3.13. This build removes the single global lock and instead uses more granular, per-object synchronization so multiple threads really can execute Python bytecode in parallel on separate cores. It is still evolving — expect a transition period where some C extensions need updates to be thread-safe without the GIL's blanket protection, and the free-threaded build is not yet the default way most people run Python. It is, however, the clearest sign that the GIL's long-term future in CPython is no longer set in stone.
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