Free-threaded Python 3.14 in practice: what still breaks



The free-threaded build of CPython -- python3.14t -- is no longer a weekend experiment. It ships from python.org's official installers and from pyenv install 3.14t, and on Ubuntu you can get it straight from the deadsnakes PPA as python3.14-nogil. It's still flagged experimental upstream, but it's stable enough that "should we turn this on" is now a real question for a working codebase, not a toy.

The honest answer, eight months into 3.14t being broadly available: it depends entirely on what's in your requirements.txt. The interpreter itself is solid. The ecosystem around it is not evenly ported, and the failure mode is sneaky -- your process doesn't crash, it just quietly goes back to running with the GIL on, and nothing tells you unless you go looking.

Terminal showing python3.14t re-enabling the GIL after importing numpy built without free-threaded support

How the GIL gets re-enabled behind your back

A C extension has to opt in to free-threading explicitly, by calling PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED) during module init. If it doesn't -- because the maintainer hasn't audited it for thread safety, or just hasn't gotten to it -- CPython prints a RuntimeWarning and re-enables the GIL for the rest of the process. Every thread you spin up after that point is back to GIL semantics, even though sys.executable still says python3.14t.

Check it with sys._is_gil_enabled(), added in 3.13:

import sys
import sysconfig

def gil_status():
    if not sysconfig.get_config_var("Py_GIL_DISABLED"):
        return "not a free-threaded build"
    if sys._is_gil_enabled():
        return "free-threaded build, but GIL is back on"
    return "free-threaded, GIL off"

print(gil_status())

What actually breaks, as of October 2026

Librarycp314t wheel on PyPINotes
numpyyes, since 2.3Free-threaded support landed properly; the buffer protocol and ufunc loops were the hard part.
pandaspartialCore DataFrame ops are fine; several accelerated paths that shell out to bottleneck/numexpr still force the GIL back on.
psycopg (v3)yesThe C extension was rewritten with free-threading in mind; psycopg2 is not and probably never will be.
lxmlnolibxml2's internal state isn't thread-safe yet upstream, so the binding can't be either.
Pillowyes, since 11.xImage decoding already released the GIL in the old build; free-threaded mode just made it official.
uvloopnoFalls back silently to the stdlib asyncio loop with a warning if you try to use it under 3.14t.

A detection script you can drop into CI

Don't rely on reading warnings in scrollback. Diff sys._is_gil_enabled() before and after each import that matters to your project:

# ci_check_free_threading.py
import importlib
import sys
import sysconfig

CRITICAL_IMPORTS = ["numpy", "psycopg", "PIL", "lxml", "cryptography"]

def main() -> int:
    if not sysconfig.get_config_var("Py_GIL_DISABLED"):
        print("skipping: not a free-threaded interpreter")
        return 0

    broke = []
    for name in CRITICAL_IMPORTS:
        before = sys._is_gil_enabled()
        try:
            importlib.import_module(name)
        except ImportError:
            continue
        if before is False and sys._is_gil_enabled() is True:
            broke.append(name)

    if broke:
        print("GIL re-enabled by:", ", ".join(broke))
        return 1
    print("all critical imports stayed free-threaded")
    return 0

if __name__ == "__main__":
    raise SystemExit(main())

We run this as a non-blocking nightly job against python3.14t, separate from the regular 3.13 test matrix that actually gates merges. It doesn't fail the build -- it just gives us a trend line of which dependencies have caught up, so we know when it's worth revisiting the GIL-off build for real, instead of re-testing every library by hand every few months.

Where we landed

Production stays on the regular GIL-enabled build for now. The blocker isn't the interpreter, it's that pandas and a couple of our image-processing dependencies still silently flip the GIL back on, which means any thread-pool code we'd write "for free-threading" degrades to normal GIL contention anyway -- except now it's quieter and harder to notice in a profiler. Once the detection script shows a clean run on the libraries we actually ship, turning it on in staging is a one-line change.

Leave a comment

Anonymous comments are published after review.