The GIL, and free-threaded Python
The most-changed Python interview topic in years. As of Python 3.14, free-threaded CPython is officially supported (PEP 779) — no longer experimental. Answering as if the GIL is immovable dates you.
Verified 2026-08. Re-check before quoting version specifics.
What the GIL is
A mutex in CPython allowing only one thread to execute Python bytecode at a time. One lock, per interpreter.
It exists because CPython’s memory management is reference counting, and reference counts are updated constantly — every assignment, every function call. Making each Py_INCREF/Py_DECREF atomic would cost more than the GIL does, so CPython took one coarse lock instead of millions of fine ones.
Consequences in the GIL build:
| Workload | Threads help? | Why |
|---|---|---|
| CPU-bound | no | only one thread runs bytecode; you get context-switch overhead and no parallelism |
| I/O-bound | yes | the GIL is released around blocking I/O, so other threads run while one waits |
| C extensions | sometimes | NumPy, and similar libraries release the GIL around heavy native work |
That third row matters: a NumPy matrix multiply releases the GIL, so threaded NumPy code does parallelise. “Python threads never parallelise” is too strong.
Free-threaded Python
Python 3.13 shipped an experimental free-threaded build. Python 3.14 (October 2025) made it officially supported via PEP 779 — phase two of the removal project. It remains an optional build, not the default.
python3.14t # the free-threaded interpreter, note the 't'
import sys
sys._is_gil_enabled() # False on a free-threaded build
What it changes: threads execute Python bytecode genuinely in parallel. CPU-bound multithreading finally scales across cores.
What it costs:
- Single-threaded performance overhead — biased reference counting and other machinery aren’t free, though the gap has narrowed substantially since 3.13.
- C extensions must be updated. An extension not declaring free-threading support causes the interpreter to re-enable the GIL at import. This is the practical blocker: your dependency tree has to be ready.
- Your code must actually be thread-safe. The GIL never made your code correct, but it made many race conditions vanishingly unlikely to be observed. Free-threaded execution surfaces them.
That last point is the one worth stressing. Code that “worked” under the GIL because a race window was one bytecode wide can now fail.
Four concurrency models, not three
Python 3.14 also landed PEP 734 — multiple interpreters in the stdlib. So the decision tree has a new branch:
| Model | Parallel Python bytecode | Memory | Cost |
|---|---|---|---|
| asyncio | no | shared, one thread | cheapest; needs async I/O |
| threads (GIL build) | no | shared | cheap; I/O only |
| threads (free-threaded) | yes | shared | cheap; needs thread-safe code + compatible extensions |
| subinterpreters | yes | isolated per interpreter | mid; each has its own GIL |
| processes | yes | isolated, IPC via pickle | most expensive; most robust |
from concurrent import interpreters # 3.14 stdlib
interp = interpreters.create()
interp.exec("import heavy; heavy.compute()")
Subinterpreters sit between threads and processes: real parallelism with much lower startup and memory cost than a process, but isolated state, so sharing data means passing it explicitly.
Choosing, in 2026
- I/O-bound, async-capable libraries → asyncio. Still the default for network services.
- I/O-bound with blocking libraries → threads. Works on either build.
- CPU-bound, and you control the deployment → free-threaded build with threads, if your extensions support it. Otherwise processes.
- CPU-bound, mixed workload, shared-nothing → subinterpreters or processes.
- CPU-bound in NumPy/Polars/PyTorch → often already parallel; measure before adding anything.
The honest position for most production code today: processes remain the safe default for CPU-bound work, because extension support and thread-safety auditing are real migration costs. But knowing free-threading is official, and what would block you from adopting it, is the current answer.
Other implementations
| Implementation | GIL |
|---|---|
| CPython (default build) | yes |
| CPython (free-threaded build) | no |
| PyPy | yes (JIT-accelerated) |
| Jython, IronPython | no |
Interview angle
- “What is the GIL and why does it exist?” — a per-interpreter mutex allowing one thread to execute bytecode at a time. It exists because CPython uses reference counting, and one coarse lock is cheaper than making every refcount update atomic.
- “Is the GIL still a thing in 2026?” — the GIL build is still the default, but free-threaded CPython became officially supported in 3.14 under PEP 779, after being experimental in 3.13. It’s an optional build, not the default, and that distinction is the precise answer.
- “What stops teams adopting free-threading?” — C extension support (an extension not declaring compatibility makes the interpreter re-enable the GIL), some single-threaded overhead, and the fact that latent race conditions the GIL made unobservable now surface. The GIL never made your code thread-safe; it made unsafety hard to notice.
- “Do Python threads ever give you parallelism on a GIL build?” — yes, for I/O, because the GIL is released around blocking calls, and for C extensions like NumPy that release it around native computation. “Threads never parallelise” is too strong.
- “What are subinterpreters and when would you use them?” — PEP 734 landed multiple interpreters in the 3.14 stdlib, each with its own GIL. They give real parallelism with isolated state, cheaper than processes and safer than shared-memory threads. Good for shared-nothing CPU work.
- “CPU-bound work in Python today — what do you reach for?” — processes remain the safe default because of extension compatibility and thread-safety auditing. Free-threaded builds with threads are the direction of travel, and subinterpreters are the middle ground. Check first whether your numeric library already parallelises.