$RodHat_
MOTD

Python finally killed the GIL. The code that needed it left years ago.

Published by

Python finally killed the GIL. The code that needed it left years ago.
Photo: AI-generated — no human photographer / RodHat AI Cover

Python 3.14 shipped. Free-threaded mode — no GIL, real parallel threads, PYTHON_GIL=0 or build with --disable-gil — is stable. Not experimental. Not a flag you type and then immediately file a bug about. Stable.

This is correct. It is also thirty years late, and for most of the Python you care about, it changes very little. Let me explain why both of those things are true.

What the GIL actually was

The Global Interpreter Lock is not a Python feature. It is an implementation detail of CPython — the C reference implementation — that existed because CPython uses reference counting for memory management, reference counting is not atomic, and making it atomic in 1991 would have required either a serious performance hit or a serious engineering effort that nobody was going to fund.

Guido van Rossum’s answer: one lock, one thread runs Python bytecode at a time. This made C extensions safe to write without thinking about thread safety, because the GIL guaranteed that no two threads were modifying reference counts simultaneously. It made CPython itself easier to implement and maintain. It also made import threading a quiet lie for CPU-bound workloads for three decades.

The GIL releases during system calls — blocking I/O, waiting on a socket, waiting on a read(). Async Python and threaded I/O Python have always worked correctly. The problem was always CPU: if you wanted to use multiple cores to do real computation in Python, the GIL serialized you to one core regardless of how many threads you spawned.

What the ecosystem did about it

Everything except fix the root problem.

multiprocessing gives you multiple OS processes instead of threads — real parallel execution, but also real memory copies for every piece of shared state, real IPC overhead, real hell when a subprocess silently dies with the wrong exit code. This has been the standard answer for CPU-bound Python for fifteen years.

NumPy, SciPy, and the scientific Python stack dropped into C-level code for heavy computation — which releases the GIL. A numpy.matmul() on a large array releases the GIL and runs its BLAS routine in parallel at the C level. The Python layer is thin; the computation is in C or Fortran. This works. It also means the GIL was effectively transparent for the workloads that actually needed parallelism at scale.

Cython, Numba, and PyPy took different routes. Each one either bypassed the GIL in extension code or replaced the interpreter entirely. The production ML pipelines that care about per-core throughput are either in CUDA kernels or they’re not really in Python.

The async world — asyncio, trio, anyio — never cared about the GIL. Async Python concurrency is cooperative scheduling on one thread. Removing the GIL changes nothing for an asyncio web server. Your framework endpoint is still running single-threaded event loop code. The GIL was never the bottleneck.

What PEP 703 actually changed

Free-threaded CPython replaces the GIL’s implicit protection of reference counts with biased reference counting: each thread maintains its own local reference count for objects it owns, with synchronization only for objects shared across threads. There’s also a deferred reference counting path for widely-shared objects that avoids the cache-line ping-pong you’d get from naive concurrent increments on a single counter.

The benchmark numbers are what you’d expect. Free-threaded Python on CPU-bound pure-Python workloads gets genuine linear scaling across cores for the first time. Single-threaded pure-Python code pays a small penalty — biased reference counting is slightly more expensive than the old GIL-protected non-atomic path — somewhere between 5% and 15% depending on the workload and the object access pattern. I/O-bound code is roughly unchanged.

C extensions are the gotcha. Most were written with the GIL as an implicit guarantee — they assume Python objects won’t be modified by another thread while they hold a reference, because the GIL said so. Free-threaded builds mark extensions as GIL-safe or not. An extension that hasn’t been audited still runs, but the interpreter acquires a per-extension lock that effectively re-serializes it to the old single-threaded behavior. You won’t crash. You also won’t get the parallelism you hoped for.

The major scientific Python packages have shipped GIL-safe wheels. The long tail of CFFI-wrapped C libraries and decade-old Cython extensions mostly hasn’t, and won’t for a while.

Who this is actually for

If you’re writing new CPU-bound Python code today — genuinely parallelizable computation that lives mostly in Python objects rather than in NumPy arrays or C extensions — you can now use threads where you’d have reached for multiprocessing. The savings are real: thread startup is cheap, shared memory is trivial, you don’t need to serialize state across process boundaries or handle subprocess lifecycle.

If you’re running an existing async Python server: nothing changes.

If you’re running a scientific computing pipeline that’s already NumPy-heavy: nothing meaningful changes. That code was already GIL-free in the parts that mattered.

If you’re maintaining a C extension that does serious work: you have a new audit in your backlog. Not urgently — your extension still works — but the “GIL-safe” flag is becoming an ecosystem quality signal and you’ll need to address it eventually.

The reluctant verdict

PEP 703 is correct. Biased reference counting is a reasonable implementation choice — the same kind of careful, backward-compatible design thinking that Git used for the SHA-256 transition: don’t break the existing world, give everyone an opt-in path, let the ecosystem migrate. The GIL was always the wrong answer to a legitimate question about concurrent memory management, and the answer that took thirty years turns out to be technically sound.

The reason I’m not more enthusiastic is that the pain the GIL caused pushed every serious use case toward workarounds that didn’t need the fix: multiprocessing, C extensions, separate processes, or switching to a different language for the compute-intensive parts. The CISA memory safety conversation is the same shape: by the time the institution catches up to the problem, production engineering has already built the workaround infrastructure. The right fix arrives; the migration path around it has been paved for years.

Use free-threaded Python when it fits the problem. Test your C extensions before assuming you’re getting the speedup. Don’t rewrite your multiprocessing code on day one just because you can — wait until the extension ecosystem has caught up and the “GIL-safe” wheel is the normal case rather than the exception.

The fix is real. The timing is what it is.


If you want to understand why concurrent memory management is hard at the implementation level — what biased reference counting is solving and why the naive alternatives are worse — Operating Systems: Three Easy Pieces covers concurrent memory allocation and locking strategies in the chapter on concurrency bugs. The CPython implementation internals aren’t covered there, but the underlying tradeoffs are exactly the ones that made the GIL feel necessary in 1991 and make its removal expensive in 2026.