Free-Threaded CPython

Free-threaded CPython is a build of the reference interpreter compiled with the --disable-gil configure flag, in which the Global Interpreter Lock (GIL) — the single mutex that has historically serialized all Python bytecode execution — is removed, so that multiple operating-system threads can run Python code on multiple CPU cores truly in parallel. It was introduced by PEP 703 (“Making the Global Interpreter Lock Optional in CPython”) and first shipped as an experimental build in Python 3.13 (October 2024). With PEP 779 (“Criteria for supported status for free-threaded Python,” accepted 16 June 2025), the build became officially supported but still opt-in in Python 3.14 (per What’s New in 3.14 and PEP 779). It is not the default interpreter: you must download or build a distinct binary, conventionally named python3.14t, to get it. Removing the GIL is not a single change but a deep re-engineering of CPython’s memory model — reference counting, object locking, and memory reclamation all had to be redesigned, which is why it took years and is the through-line of this section.

This note covers what the free-threaded build is and how you use it: the build flag and ABI, how to detect it at runtime, the runtime GIL toggle, how C extensions declare safety, and a map of what changed inside. The companion note Free-Threading Performance Trade-offs covers what it costs and gains — the single-threaded overhead, the memory overhead, and the multi-core scaling wins. Read them together.

Mental Model

The right way to think about the free-threaded build is as a second, parallel ABI of CPython that ships alongside the normal one. The default build (python3.14) keeps the GIL and behaves exactly as Python always has. The free-threaded build (python3.14t — note the trailing t for “threading”) defines the C preprocessor macro Py_GIL_DISABLED, which conditionally compiles in a different reference-counting scheme, per-object locks, and a different memory allocator. Both builds run the same .py source; the difference is entirely below the source line.

flowchart TD
    SRC["Your .py source<br/>(identical for both builds)"]
    SRC --> DEF["python3.14<br/>default build<br/>(GIL present)"]
    SRC --> FT["python3.14t<br/>free-threaded build<br/>(--disable-gil, Py_GIL_DISABLED defined)"]

    DEF --> DEFR["One global mutex<br/>serializes bytecode;<br/>plain non-atomic refcount;<br/>pymalloc"]
    FT --> FTR["No global mutex;<br/>biased + deferred refcount;<br/>per-object locks;<br/>mimalloc + QSBR"]

    FTR --> TOGGLE{"Runtime toggle:<br/>PYTHON_GIL / -X gil"}
    TOGGLE -->|"gil=0 (default)"| PARALLEL["Threads run Python<br/>in parallel on N cores"]
    TOGGLE -->|"gil=1, or GIL-needing<br/>extension imported"| REENABLED["GIL re-enabled;<br/>behaves like default build"]

Figure: the two builds diverge entirely below the source line. The insight to take away is that “free-threaded” is a compile-time choice (Py_GIL_DISABLED) that produces a separate binary and ABI; the GIL can then still be re-enabled at runtime in that binary, either deliberately or automatically when an unsafe extension is loaded.

How You Get and Identify It

The build flag and the ABI suffix

You opt into the free-threaded build at compile time. Building CPython from source, you pass --disable-gil to configure (PEP 703). The python.org installers for macOS and Windows offer a free-threaded option, and community-maintained builds exist for other platforms (tracked at py-free-threading.github.io).

The resulting binary is named with a trailing tpython3.14t on Unix, python3.14t.exe on Windows. This t is an ABI flag (mnemonically, “threading”): it appears in the interpreter name, in compiled extension filenames (a wheel built for the free-threaded ABI carries a tag like cp314t), and in sysconfig’s EXT_SUFFIX. The t exists because the free-threaded ABI is incompatible with the default one — an extension .so/.pyd compiled for python3.14 will not load into python3.14t and vice versa, because the object layout and refcounting macros differ. Keeping the two ABIs distinct prevents you from accidentally loading an incompatible binary.

Internally, the build flag sets the C preprocessor macro Py_GIL_DISABLED. Every place in the CPython source that must behave differently without a GIL is guarded by #ifdef Py_GIL_DISABLED. (Verified against the v3.14.5 source tree, e.g. Include/object.h, Objects/object.c.)

Detecting it at runtime

There are three layers of “is this free-threaded?” and they answer different questions.

Does this build support free-threading? This is a static property of the binary. The robust check is via sysconfig, which reports the compile-time macro:

import sysconfig
sysconfig.get_config_var("Py_GIL_DISABLED")   # 1 if the build supports free threading, else None/0

This is the recommended check (free-threading HOWTO) because it reflects the build, not the current runtime state. From the shell, python -VV prints free-threading build (in 3.14) — note that 3.13 printed experimental free-threading build, a one-word change that reflects the PEP 779 promotion from experimental to supported. The same substring appears in sys.version.

Is the GIL actually disabled right now? This is a runtime property — because, as we will see, the GIL can be re-enabled at runtime even in a free-threaded build. The function is:

import sys
sys._is_gil_enabled()   # True if the GIL is currently active, False if disabled

This was added in 3.13 (the leading underscore marks it as a private/implementation API; verified in Python/sysmodule.c of v3.14.5, where it is defined via Argument Clinic as sys._is_gil_enabled -> bool). On the default build it always returns True; on the free-threaded build it returns False unless the GIL was re-enabled. The distinction between “build supports free threading” and “GIL is currently off” matters: a free-threaded build that auto-enabled the GIL because of a legacy extension is still a free-threaded binary, but sys._is_gil_enabled() will report True.

The Runtime GIL Toggle

A free-threaded build does not force the GIL off — it makes the GIL optional at runtime. Two equivalent controls exist:

PYTHON_GIL=0 python3.14t script.py    # explicitly disable the GIL (this is the default in a free-threaded build)
PYTHON_GIL=1 python3.14t script.py    # re-enable the GIL for this run
python3.14t -X gil=0 script.py        # same, via -X flag
python3.14t -X gil=1 script.py        # same, via -X flag

Resolved (2026-06-01)

The environment variable is PYTHON_GIL (with underscore), paired with -X gil=[0|1]. Confirmed in Python/initconfig.c at v3.14.5: the usage text reads "-X gil=[0|1]: enable (1) or disable (0) the GIL; also PYTHON_GIL", and the config reads config_get_env(config, "PYTHON_GIL"). A dispatch brief had written PYTHONGIL (no underscore), which is wrong; the spelling above is authoritative.

The -X gil=1 toggle is genuinely useful: if a workload is single-threaded, or if you hit a thread-safety bug in a library, you can switch the GIL back on in the same binary without reinstalling Python, trading parallelism for the GIL’s implicit safety and lower single-threaded overhead.

Automatic GIL re-enablement and Py_mod_gil

The hardest compatibility problem with removing the GIL is the millions of lines of existing C-extension code that silently rely on the GIL for thread safety — they mutate global state without locks because the GIL guaranteed no two threads ran C code at once. Loading such an extension into a GIL-less interpreter would be a latent data race.

CPython’s solution is a per-module declaration. A C extension declares whether it is free-threading-safe via the Py_mod_gil module slot (part of the stable ABI since 3.13; see c-api/module). The two values are macros:

// from Include/moduleobject.h (v3.14.5)
#  define Py_MOD_GIL_USED     ((void *)0)   // module needs the GIL — the default
#  define Py_MOD_GIL_NOT_USED ((void *)1)   // module is safe without the GIL

A modern multi-phase-initialization extension declares safety in its slot table:

static PyModuleDef_Slot slots[] = {
    {Py_mod_exec, exec_module},
    {Py_mod_gil, Py_MOD_GIL_NOT_USED},   // "I have been audited; I do not need the GIL"
    {0, NULL}
};

A legacy single-phase-init extension uses PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED) inside its PyInit_* function instead. If a module does not specify Py_mod_gil at all, the import machinery defaults to Py_MOD_GIL_USED — i.e., it is assumed to need the GIL. This is the safe default: an un-audited extension is presumed unsafe.

When the free-threaded interpreter imports a module marked Py_MOD_GIL_USED (explicitly or by default), it re-enables the GIL for the rest of the process and prints a warning. The exact code path and message, from Python/import.c in v3.14.5:

if (!PyModule_Check(module) ||
    ((PyModuleObject *)module)->md_gil == Py_MOD_GIL_USED) {
    if (_PyEval_EnableGILPermanent(tstate)) {
        PyErr_WarnFormat(PyExc_RuntimeWarning, 1,
            "The global interpreter lock (GIL) has been enabled to load "
            "module '%U', which has not declared that it can run safely "
            "without the GIL. To override this behavior and keep the GIL "
            "disabled (at your own risk), run with PYTHON_GIL=0 or -Xgil=0.",
            module_name);
    }
    ...
}

Reading this: when a module is loaded whose md_gil field is Py_MOD_GIL_USED, the interpreter calls _PyEval_EnableGILPermanent. If that call actually flipped the GIL on (it returns true only on the transition, so the warning fires once), a RuntimeWarning is emitted naming the offending module. The message itself tells you the escape hatch: pass PYTHON_GIL=0 or -Xgil=0 to override and keep the GIL disabled — which you should only do if you have independently verified the extension is in fact thread-safe, hence “at your own risk.” This is the graceful-degradation story: an old extension does not crash the free-threaded interpreter; it just silently costs you the GIL (and a warning) for that process.

What Had to Change Internally

Removing the GIL is hard precisely because the GIL was doing a great deal of implicit work. Four mechanisms had to be rebuilt; each has its own note.

Reference counting. Every PyObject carries a reference count, and under the GIL incrementing/decrementing it was a plain, non-atomic ++/-- because only one thread ran at a time. Without the GIL, two threads touching the same object’s count would race, and naive atomic increments on every refcount operation would be catastrophically slow (the refcount of common objects is touched constantly). PEP 703’s answer is a hybrid: Biased Reference Counting — splitting each object’s count into a fast, non-atomic “local” count owned by the thread that created the object and a slower atomic “shared” count for other threads — combined with Deferred Reference Counting for objects that are touched constantly from the interpreter (per the free-threading HOWTO: modules, functions, methods, descriptors, and thread-local objects), where the interpreter elides refcount operations on the value stack and reconciles them at safe points. A related per-thread refcounting scheme covers heap types, code objects, and module __dict__s, merging counts at a safe point or thread exit. The free-threaded build also leans on Immortal Objects (PEP 683): singletons like None, True, False, and small integers are given a sentinel refcount and are never counted at all, eliminating contention on the most-shared objects entirely.

Per-object locking. Some operations cannot be made correct by refcounting alone — e.g. resizing a list or dict while another thread reads it. Under the GIL this was free. Free-threaded CPython gives each mutable object an internal lock and takes it around critical sections; this is Per-Object Locking in Free-Threading. The built-in containers (list, dict, set) use these internal locks so that individual operations remain atomic — though, as the HOWTO warns, this is “a description of the current implementation, not a guarantee,” and you should still use explicit threading.Lock for compound operations.

Memory allocation and reclamation. The free-threaded build replaces pymalloc with mimalloc for the object-allocation domain, because mimalloc’s per-thread heaps avoid lock contention on allocation. And because a thread might be reading an object at the exact moment another thread frees it, the build uses QSBR (Quiescent State-Based Reclamation) to defer freeing memory until no thread can still hold a reference. This is why memory is freed later in the free-threaded build, and why gc.collect() can reclaim more than you expect (covered in Free-Threading Performance Trade-offs).

The specializing interpreter. The specializing adaptive interpreter (PEP 659) rewrites hot bytecodes in place with type-specialized fast paths backed by inline caches. In a GIL-less world, two threads could write the same inline cache simultaneously and a third could read it half-written. In 3.13 this was handled crudely (the interpreter was effectively de-optimized), but in 3.14 the specializing interpreter is enabled in free-threaded mode using a lock-free specialize-once scheme: each bytecode is specialized exactly once, guarded by an atomic compare-and-exchange on the opcode (verified in Python/specialize.c, v3.14.5, which uses _Py_atomic_compare_exchange_uint8 on instr->op.code). The trade-off is covered in the sibling note.

Failure Modes and Common Misunderstandings

  • “The free-threaded build is faster.” Not on single-threaded code — it is slower there (see Free-Threading Performance Trade-offs). It is only faster on workloads that genuinely use multiple cores for CPU-bound Python.
  • “My existing extension just works.” It will load, but if it is not marked Py_MOD_GIL_NOT_USED, it silently re-enables the GIL for the whole process — you get the free-threaded binary’s overhead with none of its parallelism. Check for the RuntimeWarning.
  • sys._is_gil_enabled() tells me if I have a free-threaded build.” No — it tells you the current runtime state. A free-threaded build with the GIL re-enabled returns True. Use sysconfig.get_config_var("Py_GIL_DISABLED") to check the build.
  • Frame and iterator races. Per the HOWTO: it is not safe to read frame.f_locals of a frame running in another thread (may crash), and sharing a single iterator across threads is not thread-safe (may yield duplicate or missing elements). These are sharp edges that the GIL used to paper over.
  • Wheel availability. A pure-Python package works unchanged, but a package with C extensions needs a cp314t wheel (or it must build from source against the free-threaded ABI, requiring pip 24.1+). Ecosystem coverage is still incomplete; track it at py-free-threading.github.io/tracking.

Alternatives and When to Choose Them

Free-threading is one of four ways CPython exposes parallelism. Use the free-threaded build when you have CPU-bound work that shares large in-memory state and want real thread parallelism without serializing through pickling. If the work is embarrassingly parallel with little shared state, multiprocessing on the default build is simpler and avoids the single-threaded overhead. If you want process-like isolation in-process, subinterpreters (PEP 734) with their per-interpreter GIL are an option that does not require the free-threaded build. And for I/O-bound concurrency, threads or asyncio on the default build are fine — the GIL is released during I/O anyway, so free-threading buys little there.

Production Notes

PEP 779’s acceptance came with explicit acceptance criteria the Steering Council required for “Phase II” (supported-but-optional): single-threaded performance no more than ~15% slower than the GIL build, a memory-usage target around a 20% increase, API stability (“we have not had to radically change any existing APIs”), and comprehensive internals documentation (PEP 779). The rollout is explicitly phased: Phase I = experimental (3.13), Phase II = supported but opt-in (3.14, now), Phase III = default, which is deferred and “will revolve around community support, willingness, and showing clear benefit.” So free-threading being the default is not scheduled — it is a future decision contingent on the ecosystem. Meta has funded much of the engineering (Sam Gross authored PEP 703 and led the work).

See Also