Home/Learn/Python A–Z/The GIL — Why Python Threads Don't Run in Parallel

The GIL — Why Python Threads Don't Run in Parallel

Advanced
Concurrency

The 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.

CPU-bound: two threads are no faster than one
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".

I/O releases the GIL — 10 waits overlap into 1 second
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 Aria
1

What is the GIL and why does CPython have it?

MediumFlipkart
2

Your Python service is CPU-bound. Will adding threads help? What will?

MediumRazorpay
3

When does a thread release the GIL? Why do NumPy-heavy workloads scale across threads anyway?

HardGoogle

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.

Loading discussion…