In revision.
Crisp5 min readGo deeper →

Python GIL

CPython's Global Interpreter Lock serializes bytecode execution to one thread at a time. Threads still help with I/O; for CPU work use processes or the new free-threaded build (PEP 703).

The Global Interpreter Lock is a mutex in CPython that allows only one thread to execute Python bytecode at a time. It exists because CPython's memory management (reference counting) is not thread-safe, and locking every refcount operation individually would be slower than locking once globally.

What this means in practice

  • CPU-bound multithreading does not work. Two threads doing pure Python computation run at roughly the same speed as one, sometimes slower due to lock contention.
  • I/O-bound multithreading works fine. When a thread blocks in a syscall (read, write, recv), it releases the GIL. Other threads can run. This is why threading is still useful for web scraping, HTTP clients, file processing.
  • C extensions can release the GIL. NumPy, Pandas, image libraries do heavy work in C with the GIL released. That is why scientific Python scales.
  • Multiprocessing sidesteps the GIL. Each process has its own interpreter and its own GIL. True parallelism, but with IPC overhead.
GIL serializes bytecode execution across threads

How it switches

Pre-3.2: every 100 bytecode instructions, the running thread would release and reacquire the GIL.

Post-3.2: switching is time-based. Every 5ms (configurable via sys.setswitchinterval), the current thread releases the GIL and any waiting thread gets a chance. Plus, the GIL is released around every blocking syscall and around sleep.

The new free-threaded build

PEP 703 (accepted) makes the GIL optional. Python 3.13 ships an experimental free-threaded build (python3.13t). It removes the GIL, replaces refcount operations with atomic ops, and adds per-object locks where needed. CPU-bound threading actually scales.

The cost: single-threaded performance is ~10% slower due to atomic overhead. Most C extensions need updates to be safe under free-threading. The transition will be gradual.

When to use what

  • I/O-bound, many tasks: asyncio (single-threaded) or threading.
  • CPU-bound, parallel: multiprocessing or concurrent.futures.ProcessPoolExecutor.
  • CPU-bound, vector math: NumPy / SciPy (drops the GIL internally).
  • Future: python3.13t with threading for CPU work.

The GIL is not a bug. It is a design tradeoff that bought CPython simplicity, fast single-threaded performance, and easy C extensions. Free-threading is the bet that the tradeoff has shifted.

Learn more