freenode
AnalysisLanguages & Toolchains

Python free-threading races to define what race-free actually means

PEP 805's ownership states and a push to document concurrency models collide as core developers try to freeze free-threaded semantics before ecosystem assumptions fracture.

Free-threading builds of CPython are no longer a distant experiment, yet the community still lacks a shared answer to a basic question: what does race-free parallel Python actually guarantee? With PEP 703 and PEP 734 already reshaping the runtime, Mark Shannon has now advanced PEP 805 to impose runtime ownership and object states that make sharing between threads an explicit, checked act rather than an unchecked assumption. At the same time he has opened a parallel discussion calling for documented concurrency models for both GIL and free-threaded builds, arguing that the language has long had an object model but "no mention of concurrent access." The two efforts are not identical, yet they share the same urgency. Developers and library authors are already writing code against free-threaded interpreters whose precise ordering and safety rules remain only partially specified; if those rules stay implicit, ecosystem assumptions will fracture in incompatible directions.

PEP 805's core claim is simple and strong. "With this PEP, parallel execution of code is race free by default: objects must be explicitly declared to be safe to be shared between parallel threads, or such sharing is prohibited." Shannon presents the work as a unification that improves on the safety of PEP 703, the sharing model of PEP 734, and the performance of either. Each object gains a modest amount of additional state so that the interpreter can decide, at low cost, whether an operation is permitted. An attempt by a non-owner thread to touch a still-local object raises an IllegalAccessException before any mutation can occur. The design therefore treats race freedom as a dynamic property enforced by the runtime rather than a static proof that every programmer must rediscover.

That enforcement immediately raises questions about the depth of the guarantees. ValorZard noted that freezing is shallow: a frozen object may still hold references to mutable objects that live on other threads. "Isn’t this really really bad? Wouldn’t it be better if freezing was a deep freeze?" The fear is that a method called on an ostensibly immutable object could still reach mutable state and create a data race. dpdani countered that the first access by a non-owner thread already aborts with IllegalAccessException, so the mutation path is closed before a race can form. The exchange illustrates the tension PEP 805 must resolve: how much of the object graph must be frozen, and how much residual mutability can be tolerated without reintroducing the very races the PEP claims to eliminate.

Finalizers expose a related seam. Shannon observed that the language specification already allows finalizers never to run, and that CPython currently parks some cyclic objects with destructors into an uncollectable list. Antoine Pitrou pressed for a concrete reproducer and reminded the list that CPython users often hold stricter expectations than the specification's permissive wording. Shannon replied that the current GC behavior is CPython's own and that, if the interaction between ownership transfer and __del__ grows too complex, it may be acceptable to drop the finalizer call at this stage. The discussion leaves open whether race-free parallel execution will quietly weaken long-standing cleanup assumptions that many libraries rely upon.

While PEP 805 tries to add new runtime machinery, Shannon's second thread argues that even the existing free-threaded implementation needs an explicit memory model. "We should write down concurrency models for both with-GIL and free-threading Python. It doesn’t need to be super precise and ultra rigorous, just reasonably complete and unambiguous." Under the GIL the model is essentially sequential consistency: release and acquire of the lock establish happens-before edges, and operations that never release the GIL can be treated as atomic. Free-threaded builds are far less uniform. Analysis posted by x42005e1f shows that the current implementation "does not maintain sequential consistency. It does not even maintain release-acquire ordering in general." Descriptors for user-defined slots perform a sequentially consistent load but only a release store; most other member accesses are relaxed. List operations protected by critical sections and PyMutex tend toward release-acquire or sequential consistency, yet size fields are frequently updated with relaxed atomics. Read-modify-write operations such as dict.setdefault, slot deletion, and del dict[key] are especially delicate: users expect them to behave as indivisible units that preserve modification order, yet weaker orderings can produce surprising interleavings if the operations are not carefully classified.

The practical consequence is that two free-threaded programs can observe different visibility and atomicity guarantees depending on which built-in types and which methods they touch. Without a written model, library authors cannot know which of those differences are intentional, which are temporary implementation artifacts, and which will be frozen as permanent language rules. Tools are beginning to surface the gaps. Work derived from the PyTsan thesis is being extended into a practical thread sanitizer that understands both GIL and free-threaded models and that flags races, lock inversions, and check-then-act patterns by witnessing storage locations accessed without a happens-before relation. Such detectors will be only as useful as the memory model they are told to enforce.

The two proposals therefore pull in complementary directions. PEP 805 would make many races dynamically impossible by forcing objects through explicit ownership and freeze states before they can be shared. A documented concurrency model would tell programmers, and the tools that check their code, exactly which remaining operations are atomic, which orderings are guaranteed, and how much freedom other implementations such as MicroPython or GraalPython retain. Neither effort has yet produced a finished artifact that the Steering Council or the wider ecosystem can treat as normative. Shannon's PEP is still early; the memory-model discussion is still collecting detailed inventories of current atomics rather than converging on a single prose specification.

What remains unresolved is the balance between safety enforced by the runtime and safety described for the programmer. If PEP 805's ownership states prove too restrictive or too expensive, libraries may simply keep their own locks and ignore the new API. If the free-threaded memory model is left underspecified, different projects will hard-code incompatible assumptions about list appends, dictionary updates, and attribute stores. The window for settling both questions is closing as more code begins to run without the GIL. Until the runtime checks and the written model catch up with that reality, "race-free" will remain a slogan whose precise meaning is still being negotiated one object state and one atomic ordering at a time.