Lesson 11

Free-Threaded Python

The GIL-free build changes the object model's thread-safety guarantees from "the interpreter protects you" to "you protect yourself". Here is exactly what changed and what your extension must do about it.

Learning objective

You can explain what free-threading changed inside CPython, audit an extension for the specific patterns that stop being safe, declare support correctly, and choose between critical sections, PyMutex, and thread-local state for your own data.

Where things stand

PEP 703 proposed making the GIL optional. It shipped as an experimental build in Python 3.13 and, with PEP 779 accepted, became an officially supported but non-default build in Python 3.14. You get it as a separate interpreter: python3.14t on Unix, a separate installer option on Windows. The default python3.14 still has a GIL and everything in Lesson 10 applies unchanged.

From Python:

import sys, sysconfig
print(sysconfig.get_config_var("Py_GIL_DISABLED"))   # 1 on a free-threaded build
print(sys._is_gil_enabled())        # False if the GIL is genuinely off right now

The second line is not redundant. A free-threaded interpreter can decide at runtime to turn the GIL back on, and the usual reason is your extension.

What changed inside

You do not need to implement any of this, but you need to know it happened, because each item invalidates an assumption that older extension code relies on.

Biased reference counting
Each object records an owning thread plus two counts: a local count the owner updates non-atomically, and a shared count other threads update atomically. This avoids an atomic read-modify-write on the overwhelmingly common same-thread case. Consequence: the refcount is no longer a single readable integer.
Immortal objects
Small ints, interned strings, None, True, False, static types: refcount operations on them are no-ops (PEP 683, already true on GIL builds since 3.12).
Deferred reference counting
Functions, code objects, modules and types skip refcount updates when pushed on the interpreter stack. Their true count is only knowable during a collection pause. Consequence: Py_REFCNT() and sys.getrefcount() return numbers that are approximate and not reproducible. Any test or logic that asserts an exact refcount is invalid on this build.
mimalloc
The thread-unsafe pymalloc was replaced with mimalloc, which has per-thread heaps. Consequence: the separation between object allocation and raw allocation is now enforced rather than merely recommended.
Per-object locks and optimistic reads
list, dict and set gained internal mutexes for mutation, with lock-free fast paths for reads that use a conditional refcount increment. Consequence: individual operations on builtin containers are atomic, but sequences of them are not, and borrowed references handed out by those containers are no longer safe.
Stop-the-world garbage collection
The cycle collector pauses all threads at safe points. Consequence: detaching your thread state around long work is still mandatory, exactly as in Lesson 10 — a thread that never detaches and never executes bytecode stalls every collection.

Declaring support

When a free-threaded interpreter imports an extension that has not declared itself safe, it emits a warning and re-enables the GIL for the whole process. That is a deliberate safety default, and it means one unported dependency silently erases the benefit for everyone.

For a multi-phase module (Lesson 08), declaring support is one slot:

static PyModuleDef_Slot mymod_slots[] = {
    {Py_mod_exec, mymod_exec},
#if PY_VERSION_HEX >= 0x030D0000
    {Py_mod_gil, Py_MOD_GIL_NOT_USED},
#endif
    {0, NULL}
};

For a legacy single-phase module, it is a call inside PyInit_:

#ifdef Py_GIL_DISABLED
    PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
#endif

You can also override from outside for experimentation: PYTHON_GIL=0 / PYTHON_GIL=1 in the environment, or -X gil=0 / -X gil=1.

This slot is a promise, not a build flag

Py_MOD_GIL_NOT_USED asserts that your module is thread-safe without the GIL. Nothing verifies it. Adding it to an unaudited module converts "slower than it could be" into "races and corruption under load". Do the audit below first.

The audit

1. Borrowed references from mutable containers

This is the largest category. Under the GIL, a borrowed reference was safe as long as you did not call anything that might run Python code. Without the GIL, another thread can drop the container's reference between your two instructions. Replace them:

UnsafeUse instead
PyList_GetItem, PyList_GET_ITEMPyList_GetItemRef
PyDict_GetItem, PyDict_GetItemWithErrorPyDict_GetItemRef
PyDict_GetItemStringPyDict_GetItemStringRef
PyDict_SetDefaultPyDict_SetDefaultRef
PyWeakref_GetObject, PyWeakref_GET_OBJECTPyWeakref_GetRef
PyImport_AddModulePyImport_AddModuleRef
PyCell_GETPyCell_Get

These all return strong references, so remember the matching Py_DECREF. Note the return convention of the lookup variants: 1 found, 0 not found, -1 error, with the result written through an out-parameter.

Tuples remain safe because they are immutable — a borrowed item from a tuple you hold cannot be replaced. So do borrowed references into private objects you created and have not published to other threads.

2. Unlocked accessor macros

PyList_GET_ITEM, PyList_SET_ITEM, PySequence_Fast_GET_SIZE, PySequence_Fast_GET_ITEM and direct struct field reads do no locking whatsoever. They are legitimate on an object you just created and have not shared, and unsafe on anything reachable by another thread. Audit every use for "can another thread see this object?"

3. Sequences of operations are not atomic

/* Individually atomic, jointly a race: another thread can insert between them. */
if (PyDict_Contains(d, key) == 0) {
    PyDict_SetItem(d, key, value);
}

Use PyDict_SetDefaultRef for that particular pattern, or wrap the sequence in a critical section.

4. Static mutable state

Caches, counters, lazily-initialized singletons and "last result" memos in C globals were implicitly protected by the GIL. They are not now. Three options, in order of preference:

  1. Initialize during module exec and never mutate. CPython holds a per-module lock during import, so single initialization at that point is safe with no work from you. Read-only afterwards is free.
  2. Make it per-thread with the TSS API from Lesson 10, or thread_local. No synchronization needed, at the cost of duplicated memory.
  3. Lock it with a PyMutex.

5. Thread-unsafe third-party libraries

A library that is not reentrant was fine when the GIL serialized all calls into it. Now it needs an explicit global lock:

static PyMutex lib_lock = {0};

static int call_library(...) {
    PyMutex_Lock(&lib_lock);
    int rc = legacy_library_do_thing(...);
    PyMutex_Unlock(&lib_lock);
    return rc;
}

6. Allocator discipline

The separation is now enforced by mimalloc's segregated heaps:

Mixing them (allocating with one family and freeing with another) was always a bug; now it is a reliably fatal one.

The locking tools

PyMutex

static PyMutex m = {0};     /* must be zero-initialized; one byte */
PyMutex_Lock(&m);
...
PyMutex_Unlock(&m);

Added in 3.13. It is tiny, it is fast when uncontended, and critically it detaches the thread state while it waits, which removes the deadlock from Lesson 10 automatically. Never copy or move a PyMutex; its address is meaningful.

Critical sections

A critical section locks a specific Python object's internal mutex, giving you something close to the old GIL guarantee scoped to that object:

Py_BEGIN_CRITICAL_SECTION(dict);
    PyObject *key, *value;
    Py_ssize_t pos = 0;
    while (PyDict_Next(dict, &pos, &key, &value)) {
        ...   /* dict cannot be mutated by another thread here */
    }
Py_END_CRITICAL_SECTION();

Py_BEGIN_CRITICAL_SECTION2(a, b);   /* two objects, deadlock-free ordering */
    ...
Py_END_CRITICAL_SECTION2();

The semantics you must know:

Use critical sections when you need to make a sequence of operations on a Python object atomic. Use PyMutex for your own C data.

Testing

Shipping

Repeating the build facts from Lesson 09 because they are the ones people miss: free-threaded is a distinct ABI with a t suffix, the Limited API and abi3 are not available there, and you therefore ship separate cp314t wheels. cibuildwheel and the manylinux images support this; on Windows, from 3.14 the build backend must define Py_GIL_DISABLED=1 explicitly.

A realistic porting order
  1. Build and run your existing test suite under python3.14t without declaring support. It works — the GIL just gets re-enabled — and it flushes out ABI and compile problems first.
  2. Grep for every entry in the borrowed-reference table and for mutable C statics. Fix them.
  3. Add multi-threaded stress tests for your public API.
  4. Only then add the Py_mod_gil slot, and re-run everything, ideally under TSAN.
Hands-on

Install a free-threaded interpreter (on Windows the python.org installer offers it as an optional feature; on Unix, python3.14t from your distribution or from uv python install 3.14t) and compare:

import sys, sysconfig, threading, time, math

print(sysconfig.get_config_var("Py_GIL_DISABLED"), sys._is_gil_enabled())

def busy(n):
    s = 0.0
    for i in range(n): s += math.sqrt(i)
    return s

for nthreads in (1, 2, 4):
    ts = [threading.Thread(target=busy, args=(3_000_000,)) for _ in range(nthreads)]
    t = time.perf_counter()
    [x.start() for x in ts]; [x.join() for x in ts]
    print(nthreads, round(time.perf_counter() - t, 3))

Run it on python3.14 and on python3.14t. Then import a C extension that has not been ported (many have not) and watch sys._is_gil_enabled() flip back to True — with a warning if you run with -W all.

Further reading