Skip to content

The Global Interpreter Lock (GIL)

The GIL is a mutex in the reference implementation of Python (CPython) that allows only one thread to execute Python bytecode at a time, even on a multi-core machine. It exists because CPython’s memory management (reference counting) isn’t thread-safe by default — the GIL is the simplest way to make it safe without a slower, more complex fine-grained locking scheme.

There was no “before” — the GIL has existed since CPython’s early multi-threading support, as the tradeoff that made threading in CPython possible at all without a full rewrite of the memory model. The relevant comparison isn’t “before/after” but “what does and doesn’t get around it.”

import threading
import time
def cpu_bound_work():
total = 0
for i in range(50_000_000):
total += i
return total
start = time.time()
t1 = threading.Thread(target=cpu_bound_work)
t2 = threading.Thread(target=cpu_bound_work)
t1.start(); t2.start()
t1.join(); t2.join()
print(f"Threads: {time.time() - start:.2f}s") # roughly the SAME as running both sequentially
start = time.time()
cpu_bound_work()
cpu_bound_work()
print(f"Sequential: {time.time() - start:.2f}s") # not much slower than the threaded version above
# Multiprocessing sidesteps the GIL entirely — separate processes, separate GILs
import multiprocessing
import time
def cpu_bound_work():
total = 0
for i in range(50_000_000):
total += i
return total
if __name__ == "__main__":
start = time.time()
p1 = multiprocessing.Process(target=cpu_bound_work)
p2 = multiprocessing.Process(target=cpu_bound_work)
p1.start(); p2.start()
p1.join(); p2.join()
print(f"Multiprocessing: {time.time() - start:.2f}s") # ~half the time — true parallelism

There’s no upside being “sold” here — the GIL is a constraint, and the practical takeaway is knowing when it does and doesn’t matter:

  • I/O-bound work is unaffected: while a thread waits on a network call, file read, or time.sleep(), it releases the GIL, so threading genuinely helps with I/O-bound concurrency (web requests, file downloads).
  • CPU-bound work doesn’t parallelize across threads: pure-Python number crunching in multiple threads runs no faster than one thread, because only one can hold the GIL at a time.
  • multiprocessing sidesteps it: separate processes each get their own interpreter and GIL, so CPU-bound work genuinely runs in parallel — at the cost of higher memory use and slower inter-process communication than sharing memory in threads.
  • Many C-extension libraries (NumPy, for instance) release the GIL during their heavy computation, so threading can speed up workloads that spend most of their time inside such a library, even though they’re “CPU-bound” in a loose sense.
  • A PEP 703 effort (free-threaded / “no-GIL” CPython) is an active, ongoing project as of recent Python versions — check the exact interpreter/version in use before assuming the GIL is unconditionally present.
  • The GIL is a CPython implementation detail, not part of the Python language — other implementations (Jython, IronPython) don’t have it, though CPython is what “Python” means in the vast majority of real-world deployments.
  1. Does adding more threads speed up a pure-Python CPU-bound loop?

    AnswerNo — the GIL means only one thread executes Python bytecode at a time, so CPU-bound threads don't run in parallel.
  2. Why does threading still help for I/O-bound tasks despite the GIL?

    AnswerA thread releases the GIL while it's blocked waiting on I/O (network, disk, sleep), letting other threads run during that wait — the GIL only blocks concurrent *Python bytecode execution*, not waiting.
  3. What’s the standard-library way to get true CPU parallelism in Python despite the GIL?

    Answer`multiprocessing` — separate OS processes, each with its own interpreter and GIL, genuinely run on separate cores.