The GIL, and free-threaded Python

6 interview angles 4 min read source

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.