Building for a World That Doesn't Exist Yet

Free-threaded Python is officially supported and almost nobody can build real apps on it. One wrong import silently re-enables the GIL. The ecosystem gap is wider than people think.

Free-threaded Python is real, officially supported, and still awkward to build real applications on.

That is the gap this stack is trying to live in. The runtime is here. The application ecosystem mostly is not.

It's March 2026. Free-threaded Python has been available since October 2024, when Python 3.13 shipped the first experimental nogil build. Python 3.14, released in October 2025, made free-threading officially supported via PEP 779. That's seventeen months of availability. Five months of official support.

Try building a real application on it. I dare you.


The silent kill switch

Here's the thing nobody talks about enough: one import can silently re-enable the GIL for your entire process.

When you import a C or Rust extension that hasn't declared free-threading support, Python doesn't crash. It doesn't raise an exception. It prints a RuntimeWarning, which many logging configs swallow, and quietly turns the GIL back on.

So the failure mode is not "your app explodes." The failure mode is "your app keeps running, but the architecture you thought you were testing is gone."

import sys
print(sys._is_gil_enabled())  # False — GIL is off

import some_c_extension       # RuntimeWarning: ... re-enabled the GIL

print(sys._is_gil_enabled())  # True — GIL is back

This isn't hypothetical. It is happening right now with libraries many Python applications already depend on.


The graveyard of imports

The free-threaded wheels tracker monitors the top 360 most-downloaded PyPI packages with extensions. As of March 2026, roughly 13% will re-enable the GIL on import. That sounds small until you see what's in it.

List table has no rows

And it cascades. spaCy can't be free-threading ready until blis, srsly, cymem, preshed, and murmurhash are all ready — a chain of five C extensions that all need to declare Py_MOD_GIL_NOT_USED. Miss one and the whole stack drops back to the GIL.

Warning

Even libraries with free-threaded wheels aren't necessarily safe. Pydantic-core publishes cp313t/cp314t wheels but has reported segfaults on certain builds. "Has wheels" and "works correctly under contention" are different bars.


The dependency chain problem

Your dependency tree is only as free-threaded as its weakest link.

That is what makes the migration feel worse than a normal version bump. You are not just checking whether your top-level framework is ready. You are checking whether every compiled dependency under it is ready.

flowchart TB App["Your Application (PYTHON_GIL=0)"] App --> FastAPI App --> SQLAlchemy App --> Pydantic FastAPI --> Starlette FastAPI --> Uvicorn Pydantic --> PydanticCore["pydantic-core (Rust/PyO3)"] SQLAlchemy --> GreenLet["greenlet (C extension)"] Uvicorn --> uvloop["uvloop (C extension)"]

You set PYTHON_GIL=0. You import FastAPI. Somewhere deep in the dependency tree, one C extension without the right module slot fires, and you're back to GIL-protected execution. You will not notice unless you explicitly check sys._is_gil_enabled() after imports you do not fully control.


What the ecosystem is actually doing

Credit where it's due. The foundation work is real:

List table has no rows

But notice what is still thin: the application layer. There is no obvious web framework, template engine, or ASGI server story. There is no widely adopted ORM story built around thread contention as a first-class design constraint.

The infrastructure is being retrofitted from the bottom up. Very little is being designed from the application layer down.


What "designed for" actually means

It's not enough for a library to "work" on free-threaded Python. Passing tests without segfaults is the floor, not the ceiling.

"Designed for" means the architecture starts from a world where threads share memory and the GIL does not exist:

# Flask-style: thread-local global, patched later
from threading import local
_request_ctx = local()

def get_request():
    return _request_ctx.request  # silently wrong in threadpool
# Chirp-style: ContextVar from day one
from contextvars import ContextVar
_request: ContextVar[Request] = ContextVar("request")

def get_request() -> Request:
    return _request.get()  # propagates into threadpool via copy_context()

Django can't switch from threading.local() to ContextVar without breaking middleware that depends on SimpleLazyObject. Flask can't freeze its request object without breaking extensions that mutate g. Uvicorn can't switch from processes to threads without rethinking its supervisor model.

The b-stack does not carry that baggage. Every library in it was written assuming the GIL would eventually stop being a safety blanket.


Why not wait?

Because the decisions that matter are architectural, and architecture is hardest to change after the fact.

If you wait for free-threading to be "ready" to the point where every library works, every edge case is handled, and every tool has been updated, you will mostly get retrofits. The frameworks that emerge will be Django and Flask descendants with ContextVar patches and "thread-safe mode" flags.

The opportunity right now is to build something that's native to the free-threaded world. Not "works with nogil" but "designed for it." The difference is the same as building a mobile app versus making a desktop app responsive. Technically equivalent. Architecturally different.

Warning

Being early is painful. Things break in ways that have no Stack Overflow answers. CPython itself has bugs you'll be the first to report. This is not the right choice for a production system that needs to work today with the broadest possible library compatibility.


The bet

The bet is simple: free-threaded Python becomes the default soon enough that native designs matter more than temporary compatibility workarounds.

PEP 779 Phase II is underway. The core team is committed. NumPy, PyTorch, and Cython are already there. The trajectory looks clear even if the exact timeline is not.

When that transition happens, most of the Python web ecosystem will retrofit. Thread safety will be patched in. Shared mutable state will be wrapped in locks. threading.local() will be migrated to ContextVar. It'll work. It'll carry the scars.

The b-stack, Kida through Bengal, declares _Py_mod_gil = 0 in every module. Zero C extensions. Zero Rust bindings. Pure Python, designed for a world where threads are real and the GIL is gone.

No import in this stack will silently re-enable the GIL. That's not a feature. It's the whole point.


Further reading