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.
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
| Library | cp314t wheel on PyPI | Notes |
|---|---|---|
| numpy | yes, since 2.3 | Free-threaded support landed properly; the buffer protocol and ufunc loops were the hard part. |
| pandas | partial | Core DataFrame ops are fine; several accelerated paths that shell out to bottleneck/numexpr still force the GIL back on. |
| psycopg (v3) | yes | The C extension was rewritten with free-threading in mind; psycopg2 is not and probably never will be. |
| lxml | no | libxml2's internal state isn't thread-safe yet upstream, so the binding can't be either. |
| Pillow | yes, since 11.x | Image decoding already released the GIL in the old build; free-threaded mode just made it official. |
| uvloop | no | Falls 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.
