How Python 3.13 Disables the GIL at Compile Time
Python 3.13 introduces an experimental free-threaded build mode based on PEP 703, allowing the CPython interpreter to execute multi-threaded Python code without the bottleneck of the Global Interpreter Lock (GIL). Disabling the lock requires building CPython from source using a dedicated configuration flag that replaces the standard locking architecture with atomic operations, biased reference counting, and thread-safe memory management. This article breaks down the compile-time build configuration, preprocessor macros, and internal CPython architectural shifts that remove the GIL.
The
--disable-gil Configuration Flag
To compile CPython 3.13 without the GIL, the source must be built
using the --disable-gil configuration flag during the
configure phase:
./configure --disable-gil
make
make test
sudo make installWhen this flag is passed to the build script, it triggers changes in the C compiler toolchain, fundamentally altering how the interpreter handles concurrency and memory management.
The
Py_GIL_DISABLED Preprocessor Macro
The --disable-gil option sets the C preprocessor macro
Py_GIL_DISABLED to 1 across all compiled
translation units. In standard builds, CPython relies on
PyThreadState and internal macros (such as
Py_BEGIN_ALLOW_THREADS and
Py_END_ALLOW_THREADS) to release and acquire the GIL mutex
around blocking system calls or long-running computations.
Under Py_GIL_DISABLED:
- GIL acquisition and release functions become no-ops or route to alternative synchronization paths.
- Mutex structures previously used to enforce single-thread execution
in
ceval.care omitted from the runtime loop. - Code blocks guarded by
#ifdef Py_GIL_DISABLEDreplace standard single-threaded assumptions with concurrent data structures.
Thread-Safe Memory and Biased Reference Counting
Because standard non-atomic reference counting
(Py_INCREF and Py_DECREF) causes race
conditions when multiple threads execute simultaneously without the GIL,
compiling with Py_GIL_DISABLED activates alternative
reference-counting mechanisms:
- Biased Reference Counting (BRC): Objects are marked with the thread ID that created them. The owning thread increments and decrements reference counts using non-atomic operations, while non-owning threads record increments and decrements via thread-local message queues or atomic instructions, minimizing performance degradation from atomic contention.
- Immortal Objects: Foundational objects (like
None,True,False, small integers, and built-in types) have their reference counts fixed with special bit flags so that modifications to their counts are bypassed entirely. - Mimalloc Integration: Memory allocation is swapped
to a thread-safe allocator based on Microsoft's
mimalloc. This provides isolated thread-local memory pools, preventing threads from contending for global allocation locks when creating or destroying objects.
Distinct ABI Designation
Because the internal object layout and reference counting changes break standard C API compatibility, the build system differentiates the compiled output using a distinct Application Binary Interface (ABI) tag.
A free-threaded build appends a t suffix to binary names
and wheel tags (e.g., python3.13t and
cp313-cp313t). This ABI distinction guarantees that binary
C extensions compiled for standard GIL-enabled Python cannot be
accidentally loaded into a free-threaded interpreter, preventing
undefined behavior and memory corruption.
Runtime Fallback Support
Although the lock is dismantled at compile time, the free-threaded
binary retains an optional, coarse-grained lock mechanism that can be
dynamically toggled at runtime. Users running a
--disable-gil build can pass the environment variable
PYTHON_GIL=1 or the command-line flag -X gil=1
to reactivate a global lock if they encounter compatibility problems
with legacy C extensions. By default, however, the compiled binary
executes entirely lock-free.