Python 3.15 landed on October 9, and for the free-threading effort it is the most consequential release since the whole project started. The build without the global interpreter lock was a science experiment in 3.13, an officially supported build in 3.14, and in 3.15 it finally gets what every other CPython build has had for years: a stable ABI. If you have been watching the “no-GIL” work from a distance, this is the release where it starts to look like an actual migration path rather than a research branch.
This post covers what changed in 3.15, why the ABI work matters more than it sounds, and the part nobody gets to skip: the fact that most existing Python code is quietly not thread-safe, and the GIL has been hiding that from you for decades.
The three-phase plan, and where 3.15 fits
The original free-threading proposal (PEP 703) described how to remove the GIL while keeping acceptable single-threaded performance: biased reference counting, deferred refcount updates, per-object locking, a stop-the-world mode for certain garbage collection phases. CPython 3.13 shipped it as an experimental, separate build. PEP 779 then defined the criteria for promotion, and Python 3.14 declared the free-threaded build officially supported — with an explicit promise that it will not be removed without a deprecation schedule. Whether it ever becomes the default build remains undecided; the steering council has deliberately left phase III open.
Python 3.15 pushes on the two remaining constraints: the C extension ecosystem and installation friction. Python 3.15 ships a stable ABI for free-threaded builds, and the official macOS binaries now install free-threading support by default — a small sentence that means every new Mac gets a no-GIL-capable interpreter without hunting for community builds.
What actually changed under the hood in 3.15
The headline item is abi3t, the Stable ABI for Free-Threaded Builds. Until now, extension authors targeting free-threaded CPython had to publish version-specific wheels (a cp314t wheel for 3.14, another for 3.15), because the free-threaded build had no stable ABI at all. With PEP 803, a single abi3t binary can serve both the GIL-enabled and free-threaded builds and future CPython versions, the same way the classic abi3 wheels do for the GIL build.
Getting there is not free. The official migration guide details most of what real source changes look like: switching from embedding PyObject directly in instance structs to the type-data APIs introduced by PEP 697 (negative basicsize and PyObject_GetTypeData()), and replacing the PyInit_ module entry point with the new PyModExport_* hook from PEP 793 and the PySlot structure from PEP 820. The upside is a wheel story that finally scales: build tools including meson-python, scikit-build-core, and Maturin already support selecting abi3t.
Two smaller items round out the release. The PyGILState_Ensure()/PyGILState_Release() family — the C API you would use to grab the GIL from foreign threads — is now soft-deprecated: it keeps working, but it will not grow new APIs, because its whole model assumes a lock that free-threaded builds do not have. And the threading module gained serialize_iterator(), synchronized_iterator(), and concurrent_tee(), acknowledging that sharing iterators across threads — previously pointless when threads could not run Python code in parallel — is now a real use case.
Which build am I running?
Before anything else, you need to know which interpreter you actually have. Free-threaded builds install side by side with the default build (the interpreter binary gets a t suffix, e.g. python3.15t), and importing one incompatible C extension can re-enable the GIL at runtime (a RuntimeWarning tells you it happened). Two checks cover both situations:
import sys
import sysconfig
# Compile-time check: is this a free-threaded build?
print(sysconfig.get_config_var("Py_GIL_DISABLED")) # 1 on free-threaded, 0 or None otherwise
# Runtime check: the GIL can be re-enabled on a free-threaded build,
# for example after importing an extension that lacks free-threading support
print(sys._is_gil_enabled())
When a native extension does not declare support for running without the GIL, the free-threaded interpreter enables the GIL and emits a RuntimeWarning telling you it has done so. You can force the GIL off despite that with the PYTHON_GIL=0 environment variable or -X gil=0 — a fine move for an experiment, a terrible move for production, since the check exists because the extension’s internals may genuinely assume serialized access.
Your Python code is probably not thread-safe
This is the part that deserves more attention than any ABI detail. On the GIL build, one thread executes Python bytecode at a time, so a surprising number of race conditions degrade into “harmless” interleavings that never corrupt anything. Remove the GIL and those same races become real data corruption. Read-modify-write sequences like self.value += 1 are not atomic, and codebases carry threading bugs that were, until now, purely theoretical.
from concurrent.futures import ThreadPoolExecutor
from threading import Lock
class Counter:
def __init__(self) -> None:
self.value = 0
self.lock = Lock()
def increment(self) -> None:
with self.lock:
self.value += 1
counter = Counter()
with ThreadPoolExecutor(max_workers=8) as pool:
for _ in range(100_000):
pool.submit(counter.increment)
print(counter.value) # 100000, on every run
Delete the lock and this program becomes a coin flip on the free-threaded build — and, more uncomfortably, so does anything that mutates shared dicts, lists, or object attributes from multiple threads without a lock. Library authors are expected to document their thread-safety guarantees explicitly, and there are trove classifiers for exactly this: Programming Language :: Python :: Free Threading :: 1 - Unstable through 4 - Resilient. Notably, some major libraries made the pragmatic call to not guarantee it at all: NumPy, for one, does not support mutating a shared ndarray, which avoids adding locking overhead to every existing single-threaded workload.
If you maintain a library, the cheapest correct move is often conditional: detect the free-threaded build with sysconfig.get_config_var("Py_GIL_DISABLED") and either add locking or disable the shared cache only in that configuration, leaving the GIL build untouched.
The extension ecosystem is the real gate
ABI work aside, adoption still comes down to wheels. A native extension must explicitly declare that it is safe to run without the GIL, and plenty of packages have not done that yet. The community free-threading guide maintains a compatibility tracking table of the most-downloaded packages, plus a wheel tracker showing which projects publish free-threaded binaries. Toolchains have largely caught up — Cython has supported free-threaded builds since 3.1.0, and the major build backends can target abi3t — but a single pinned, unmigrated dependency in your tree is enough to force the GIL back on.
Practically, that makes your dependency audit the first migration step, not the last. Check the tracking table for everything that compiles C code, watch for the RuntimeWarning on a trial run, and treat any -X gil=0 workaround as a to-do list rather than a fix.
A low-risk way to try it this month
The builds coexist, so there is no fork in the road to take. Install the t interpreter next to your current one, point your CI job for integration tests at it, and let the thread pool in your test suite do the work — that alone tends to surface the shared-state bugs described above. One handy tool for the GIL-enabled build in 3.15: the new Tachyon sampling profiler has a dedicated mode (--mode gil) that measures how much wall time each thread spends holding the GIL, which tells you whether threads are contending in the first place — useful evidence for deciding whether free-threading would even speed up your workload, or whether it is I/O-bound and asyncio was always the answer.
Then benchmark the free-threaded build against the GIL build on your real workload. Single-threaded performance on the free-threaded build is close but not identical, and the only number that matters is yours. If your service is dominated by pure-Python CPU work across a handful of parallel requests, the win can be close to linear scaling. If it spends its life waiting on Postgres and Redis, nothing changes — which is also useful to know.
Phase III — free-threading as the default build — remains undecided, and nothing in 3.15 forces a deadline. But the pieces that used to be missing (official support, a wheel story, mainstream installer defaults) are now in place. The migration is no longer blocked on CPython; it is blocked on the ecosystem, and the ecosystem moves when maintainers start getting bug reports. Pointing your CI at python3.15t this week is how you become one of those reports instead of one of the surprises.