The GIL — Why Python Threads Don't Run in Parallel
AdvancedThe Global Interpreter Lock lets only one thread execute Python bytecode at a time — so threads never speed up CPU-bound code, but still shine for I/O because the GIL is released while waiting.
Overview
The GIL is CPython's single lock around the interpreter: at any instant, exactly one thread runs Python bytecode, no matter how many cores the machine has. It exists because CPython's memory management (reference counting) is not thread-safe, and one big lock is simpler and faster for single-threaded code than fine-grained locking everywhere. The practical consequence is the most asked concurrency question in Indian interviews: threads give zero speedup for pure computation, yet real speedup for network/disk waits — because a thread drops the GIL the moment it blocks on I/O. Python 3.13 ships an experimental free-threaded build (PEP 703) that removes the GIL, but the standard build you will deploy in 2026 still has it.
Proof: Threads Do Not Speed Up CPU Work
Two threads counting down together take as long as (often longer than) doing it sequentially — they take turns holding the GIL, plus pay switching overhead. Run this once and the GIL stops being abstract.
import threading
import time
def count(n):
while n:
n -= 1
N = 20_000_000
start = time.perf_counter()
count(N)
count(N)
print(f"sequential: {time.perf_counter() - start:.2f}s")
start = time.perf_counter()
t1 = threading.Thread(target=count, args=(N,))
t2 = threading.Thread(target=count, args=(N,))
t1.start(); t2.start()
t1.join(); t2.join()
print(f"2 threads: {time.perf_counter() - start:.2f}s")
# Typical output on a 8-core machine:
# sequential: 1.9s
# 2 threads: 2.0s ← NO speedup. Only one thread runs bytecode at a time.When the GIL Lets Go — I/O and C Extensions
A thread releases the GIL when it blocks on I/O (network, disk, sleep) and inside many C-extension calls (NumPy number crunching, hashing, compression). That is why threads are excellent for "wait on 50 APIs" and useless for "parse 50 files with pure Python".
import threading
import time
def io_task():
time.sleep(1) # blocking I/O — the GIL is RELEASED while sleeping
start = time.perf_counter()
threads = [threading.Thread(target=io_task) for _ in range(10)]
for t in threads: t.start()
for t in threads: t.join()
print(f"10 I/O tasks in {time.perf_counter() - start:.1f}s") # ~1.0s, not 10s
# Rule of thumb:
# I/O-bound (APIs, DB, files) → threading or asyncio
# CPU-bound (parsing, math) → multiprocessing (real parallel processes)
# CPU-bound + NumPy/pandas → often fine in threads (C code drops the GIL)
# Python 3.13+: experimental free-threaded build (PEP 703)
# python3.13t — no GIL, real parallel threads, ~single-digit % single-thread cost
# Standard builds still have the GIL; design for it.Key Points to Remember
- 1GIL = one lock, one thread executing Python bytecode at a time — per process
- 2Exists because CPython's reference counting is not thread-safe
- 3Threads: zero speedup for CPU-bound code, real speedup for I/O-bound code
- 4Escape hatches: multiprocessing, C extensions (NumPy), Python 3.13 free-threading
Interview Questions
Sign in to ask AriaWhat is the GIL and why does CPython have it?
Your Python service is CPU-bound. Will adding threads help? What will?
When does a thread release the GIL? Why do NumPy-heavy workloads scale across threads anyway?
Ask Aria about The GIL — Why Python Threads Don't Run in Parallel
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.