Free-Threaded Python (PEP 703): GIL Removal and Per-Object Locking
- Last updated
- Prerequisites:
- CPython GIL architecture
- Thread safety & race conditions
- python
- pep-703
- free-threading
- no-gil
- cpython
- concurrency
- multithreading
Read at your depth
The practical view
Free-threaded Python (introduced experimentally in Python 3.13 via PEP 703 and built with `--disable-gil`) allows standard Python threads (`threading.Thread`) to execute in true parallel across multiple CPU cores without being blocked by the Global Interpreter Lock (GIL). You can check if the GIL is disabled using `import sys; sys._is_gil_enabled()`.
The same idea in other frameworks
CPython `--disable-gil` (free-threaded build with biased reference counting).JVM multi-threaded platform threads running without interpreter locks.Go GMP scheduler running goroutines across multiple OS threads.Rust `std::thread` executing safe parallel code via `Send`/`Sync`.Legacy vs modern
GIL-Locked Threading (Standard Python) vs True Parallel Multi-Threading (Python 3.13t)
Standard Python threads run sequentially on 1 core; free-threaded Python achieves true linear multi-core scaling.
python3.13 app.py # ❌ CPU-bound threads serialize on 1 corepython3.13t parallel_benchmark.py # ✅ NEW: Runs in parallel across 4 cores (~1.1s)Interview gotchas
Context
This is the most critical conceptual trap regarding PEP 703.
The mechanical answer
**NO.** Removing the GIL only makes the *Python C runtime and interpreter internals* thread-safe. **User-level race conditions still exist.** If two threads mutate a shared Python dictionary or list without a `threading.Lock`, they can overwrite each other's data (lost updates). While the interpreter won't crash with memory corruption (due to internal `PyMutex` locks), your application business logic still requires explicit synchronization locks.
Trap
Removing the GIL eliminates interpreter serialization, but increases the necessity of `threading.Lock` in user code.
Context
Checks understanding of high-performance concurrent memory management.
The mechanical answer
Using pure atomic instructions (`LOCK XADD`) for every reference count increment/decrement would cause a 40%+ performance slowdown on single-threaded code. **Biased Reference Counting** assigns ownership of an object to the thread that created it. The owning thread updates a local non-atomic reference count with zero locking overhead. Only when another thread accesses the object does it fall back to an atomic shared reference count.
Trap
Biased reference counting ensures single-threaded Python code incurs only a ~5-10% performance penalty in free-threaded builds.