How Python Free-Threaded Builds Remove the GIL
Free-threaded builds in modern Python, introduced experimentally in Python 3.13 via PEP 703, remove the Global Interpreter Lock (GIL) to achieve true multi-core CPU parallelism for multithreaded code. This article explains the technical mechanics behind this architectural shift, detailing how CPython replaces the blanket protection of the GIL with biased reference counting, immortal objects, thread-safe memory management, and fine-grained locking.
The Problem the GIL Solved
CPython traditionally used the GIL to protect its internal state and memory management systems. Because Python relies on reference counting to track object lifecycles, every time a thread accesses or releases an object, its reference counter changes. Without the GIL, multiple threads modifying these counters simultaneously would cause race conditions, resulting in memory leaks or premature memory deallocation (use-after-free errors). The GIL prevented this by enforcing that only one thread could execute Python bytecode at a time.
Biased Reference Counting
The primary challenge in removing the GIL was maintaining reference count accuracy without adding severe performance overhead from atomic operations. Free-threaded Python solves this using biased reference counting:
- Owner Threads: When an object is created, it is "biased" toward the thread that allocated it.
- Fast Path: The owning thread modifies the reference count using standard, non-atomic CPU instructions, avoiding expensive atomic synchronization.
- Shared Access: When other threads access the object, they use a separate reference counter or thread-local queues that track increments and decrements atomically. When an object becomes heavily shared across multiple threads, the runtime can "unbias" the object, transitioning it to standard atomic reference counting.
Immortal Objects
CPython creates many objects that exist for the entire duration of a
program's execution, such as None, True,
False, small integers, and core runtime types. In standard
builds, these objects still undergo continuous reference count
updates.
Free-threaded builds rely heavily on immortal objects (introduced in PEP 683). These objects have a dedicated bit pattern in their reference counter marking them as permanent. The runtime simply skips reference count increments and decrements for immortal objects, completely eliminating concurrent memory access conflicts for the interpreter's most frequently shared values.
Thread-Safe Memory Allocation via mimalloc
The standard Python memory allocator (pymalloc) was
designed under the assumption that the GIL guarantees single-threaded
access. Free-threaded builds replace this architecture with
mimalloc, a high-performance, thread-safe memory
allocator originally developed by Microsoft.
mimalloc provides thread-local allocation pools and free
lists. This design allows threads to allocate and free memory blocks
concurrently without contending for a single global allocator lock,
dramatically reducing thread contention during object creation.
Fine-Grained Locking and Critical Sections
Without the universal lock of the GIL, shared data structures like dictionaries, lists, and user-defined objects need protection from concurrent mutations. Free-threaded Python replaces the single global lock with critical sections and fine-grained mutexes:
- Per-Object Protection: Mutating operations acquire brief, targeted locks on the specific object being modified rather than stalling the entire runtime.
- Lock-Free Reads: Common operations, such as reading an attribute or looking up a key in a dictionary, are optimized using optimistic lock-free techniques to ensure that concurrent readers do not block one another.
Thread-Safe Garbage Collection
CPython's cyclical garbage collector (GC) must periodically scan objects to identify and clean up cyclic references that reference counting alone cannot resolve. In free-threaded builds, the GC cannot safely inspect objects while threads are modifying them in the background. To handle this, the runtime coordinates brief stop-the-world phases—pausing worker threads momentarily to safely traverse memory graphs and isolate reference cycles without data corruption.