Skip to content
Merged
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8040796
Add reference to freethreading-stable-abi-howto
clin1234 Apr 12, 2026
7175e14
Add documentation for Free Threading Stable ABI usage
clin1234 Apr 12, 2026
e0f3bd2
Update documentation on PyObject opaqueness and module init
clin1234 Apr 12, 2026
3739aec
Fix formatting for PyObject opaqueness section
clin1234 Apr 12, 2026
c235a75
Fix formatting of code snippet in documentation
clin1234 Apr 12, 2026
753156b
Update documentation on PyObject opaqueness
clin1234 Apr 12, 2026
344105e
Py_Type
clin1234 Apr 12, 2026
29b932d
Clarify free-threaded limited API build documentation
clin1234 Apr 12, 2026
8545eeb
Revise freethreading-stable-abi.rst for clarity
clin1234 Apr 12, 2026
2a11368
Add freethreading-stable-abi.rst to documentation index
clin1234 Apr 12, 2026
2f2e756
Simplify threading and API guidelines in documentation
clin1234 May 1, 2026
5d23810
Update freethreading-stable-abi.rst
clin1234 May 1, 2026
a8294fa
Update freethreading-stable-abi.rst
clin1234 May 2, 2026
43538cf
Update freethreading-stable-abi.rst
clin1234 May 2, 2026
ed0e4fe
Update module slot definitions to use PySlot from PEP 820
clin1234 May 17, 2026
a7fcbcc
Update freethreading-stable-abi.rst
clin1234 May 17, 2026
ccdedb0
Fix typo in freethreading-stable-abi.rst
clin1234 May 17, 2026
3b401db
Update freethreading-stable-abi.rst
clin1234 May 17, 2026
a7fdaaf
Merge in the main branch
encukou May 29, 2026
cb9545a
Rewrite as a guide
encukou May 27, 2026
22a9bfe
Clarification
encukou May 29, 2026
1448abd
No static types
encukou May 29, 2026
fcc553a
Apply suggestions from code review
encukou Jun 1, 2026
6d8dc71
Address reviews
encukou Jun 1, 2026
d89c007
Add link to What's New
encukou Jun 1, 2026
370ef9b
Apply suggestions from code review
encukou Jun 2, 2026
51a55ac
Remove heading-like incomplete sentence
encukou Jun 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Simplify threading and API guidelines in documentation
Removed sections on API guidelines, critical sections, and thread safety from the documentation.
  • Loading branch information
clin1234 committed May 17, 2026
commit 2f2e7560a03f27e0553b35dd9fc64ffa49de8afb
255 changes: 17 additions & 238 deletions Doc/howto/freethreading-stable-abi.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,259 +98,37 @@ for :ref:`single-phase initialization <single-phase-initialization>`
(that is, :c:func:`PyModule_Create`), they are not exposed when targeting the regular Stable ABI.
Prefer multi-phased initializtion when possible.

General API Guidelines
======================

Most of the C API is thread-safe, but there are some exceptions.

* **Struct Fields**: Accessing fields in Python C API objects or structs
directly is not thread-safe if the field may be concurrently modified.
* **Borrowed References**: C API functions that return
:term:`borrowed references <borrowed reference>` may not be thread-safe if
the containing object is modified concurrently. See the section on
:ref:`borrowed references <borrowed-references>` for more information.


Container Thread Safety
.......................

Containers like :c:struct:`PyListObject`,
:c:struct:`PyDictObject`, and :c:struct:`PySetObject` perform internal locking
in the free-threaded build. For example, the :c:func:`PyList_Append` will
lock the list before appending an item.

Borrowed References
===================

.. _borrowed-references:
Critical Sections
=================

Some C API functions return :term:`borrowed references <borrowed reference>`.
These APIs are not thread-safe if the containing object is modified
concurrently. For example, it's not safe to use :c:func:`PyList_GetItem`
if the list may be modified concurrently.
.. _critical-sections:

The following table lists some borrowed reference APIs and their replacements
that return :term:`strong references <strong reference>`.
Replacements:

+-----------------------------------+-----------------------------------+
| Borrowed reference API | Strong reference API |
| Macro functions | C API functions |
+===================================+===================================+
| :c:func:`PyList_GetItem` | :c:func:`PyList_GetItemRef` |
| :c:func:`Py_BEGIN_CRITICAL_SECTION` | :c:func:`PyCriticalSection_Begin` |
| :c:func:`Py_END_CRITICAL_SECTION` | |:c:func:`PyCriticalSection_End` |
+-----------------------------------+-----------------------------------+
| :c:func:`PyList_GET_ITEM` | :c:func:`PyList_GetItemRef` |
| :c:func:`Py_BEGIN_CRITICAL_SECTION2` | :c:func:`PyCriticalSection2_Begin` |
| :c:func:`Py_END_CRITICAL_SECTION2` | |:c:func:`PyCriticalSection2_End` |
+-----------------------------------+-----------------------------------+
| :c:func:`PyDict_GetItem` | :c:func:`PyDict_GetItemRef` |
| :c:func:`Py_BEGIN_CRITICAL_SECTION_MUTEX` | :c:func:`PyCriticalSection_BeginMutex` |
| :c:func:`Py_END_CRITICAL_SECTION` | |:c:func:`PyCriticalSection_End` |
+-----------------------------------+-----------------------------------+
| :c:func:`PyDict_GetItemWithError` | :c:func:`PyDict_GetItemRef` |
| :c:func:`Py_BEGIN_CRITICAL_SECTION2_MUTEX` | :c:func:`PyCriticalSection2_BeginMutex` |
| :c:func:`Py_END_CRITICAL_SECTION2` | |:c:func:`PyCriticalSection2_End` |
+-----------------------------------+-----------------------------------+
| :c:func:`PyDict_GetItemString` | :c:func:`PyDict_GetItemStringRef` |
+-----------------------------------+-----------------------------------+
| :c:func:`PyDict_SetDefault` | :c:func:`PyDict_SetDefaultRef` |
+-----------------------------------+-----------------------------------+
| :c:func:`PyDict_Next` | none (see :ref:`PyDict_Next`) |
+-----------------------------------+-----------------------------------+
| :c:func:`!PyWeakref_GetObject` | :c:func:`PyWeakref_GetRef` |
+-----------------------------------+-----------------------------------+
| :c:func:`!PyWeakref_GET_OBJECT` | :c:func:`PyWeakref_GetRef` |
+-----------------------------------+-----------------------------------+
| :c:func:`PyImport_AddModule` | :c:func:`PyImport_AddModuleRef` |
+-----------------------------------+-----------------------------------+
| :c:func:`PyCell_GET` | :c:func:`PyCell_Get` |
+-----------------------------------+-----------------------------------+

Not all APIs that return borrowed references are problematic. For
example, :c:func:`PyTuple_GetItem` is safe because tuples are immutable.
Similarly, not all uses of the above APIs are problematic. For example,
:c:func:`PyDict_GetItem` is often used for parsing keyword argument
dictionaries in function calls; those keyword argument dictionaries are
effectively private (not accessible by other threads), so using borrowed
references in that context is safe.

Some of these functions were added in Python 3.13. You can use the
`pythoncapi-compat <https://github.com/python/pythoncapi-compat>`_ package
to provide implementations of these functions for older Python versions.


Thread State and GIL APIs
=========================

Python provides a set of functions and macros to manage thread state and the
GIL, such as:

* :c:func:`PyGILState_Ensure` and :c:func:`PyGILState_Release`
* :c:func:`PyEval_SaveThread` and :c:func:`PyEval_RestoreThread`
* :c:macro:`Py_BEGIN_ALLOW_THREADS` and :c:macro:`Py_END_ALLOW_THREADS`

These functions should still be used in the free-threaded build to manage
thread state even when the :term:`GIL` is disabled. For example, if you
create a thread outside of Python, you must call :c:func:`PyGILState_Ensure`
before calling into the Python API to ensure that the thread has a valid
Python thread state.

You should continue to call :c:func:`PyEval_SaveThread` or
:c:macro:`Py_BEGIN_ALLOW_THREADS` around blocking operations, such as I/O or
lock acquisitions, to allow other threads to run the
:term:`cyclic garbage collector <garbage collection>`.


Protecting Internal Extension State
===================================

Your extension may have internal state that was previously protected by the
GIL. You may need to add locking to protect this state. The approach will
depend on your extension, but some common patterns include:

* **Caches**: global caches are a common source of shared state. Consider
using a lock to protect the cache or disabling it in the free-threaded build
if the cache is not critical for performance.
* **Global State**: global state may need to be protected by a lock or moved
to thread local storage. C11 and C++11 provide the ``thread_local`` or
``_Thread_local`` for
`thread-local storage <https://en.cppreference.com/w/c/language/storage_duration>`_.


Critical Sections
=================

.. _critical-sections:

In the free-threaded build, CPython provides a mechanism called "critical
sections" to protect data that would otherwise be protected by the GIL.
While extension authors may not interact with the internal critical section
implementation directly, understanding their behavior is crucial when using
certain C API functions or managing shared state in the free-threaded build.

What Are Critical Sections?
...........................

Conceptually, critical sections act as a deadlock avoidance layer built on
top of simple mutexes. Each thread maintains a stack of active critical
sections. When a thread needs to acquire a lock associated with a critical
section (e.g., implicitly when calling a thread-safe C API function like
:c:func:`PyDict_SetItem`, or explicitly using macros), it attempts to acquire
the underlying mutex.

Using Critical Sections
.......................

The primary APIs for using critical sections are:

* :c:macro:`Py_BEGIN_CRITICAL_SECTION` and :c:macro:`Py_END_CRITICAL_SECTION` -
For locking a single object

* :c:macro:`Py_BEGIN_CRITICAL_SECTION2` and :c:macro:`Py_END_CRITICAL_SECTION2`
- For locking two objects simultaneously

These macros must be used in matching pairs and must appear in the same C
scope, since they establish a new local scope. These macros are no-ops in
non-free-threaded builds, so they can be safely added to code that needs to
support both build types.

A common use of a critical section would be to lock an object while accessing
an internal attribute of it. For example, if an extension type has an internal
count field, you could use a critical section while reading or writing that
field::

// read the count, returns new reference to internal count value
PyObject *result;
Py_BEGIN_CRITICAL_SECTION(obj);
result = Py_NewRef(obj->count);
Py_END_CRITICAL_SECTION();
return result;

// write the count, consumes reference from new_count
Py_BEGIN_CRITICAL_SECTION(obj);
obj->count = new_count;
Py_END_CRITICAL_SECTION();


How Critical Sections Work
..........................

Unlike traditional locks, critical sections do not guarantee exclusive access
throughout their entire duration. If a thread would block while holding a
critical section (e.g., by acquiring another lock or performing I/O), the
critical section is temporarily suspended—all locks are released—and then
resumed when the blocking operation completes.

This behavior is similar to what happens with the GIL when a thread makes a
blocking call. The key differences are:

* Critical sections operate on a per-object basis rather than globally

* Critical sections follow a stack discipline within each thread (the "begin" and
"end" macros enforce this since they must be paired and within the same scope)

* Critical sections automatically release and reacquire locks around potential
blocking operations

Deadlock Avoidance
..................

Critical sections help avoid deadlocks in two ways:

1. If a thread tries to acquire a lock that's already held by another thread,
it first suspends all of its active critical sections, temporarily releasing
their locks

2. When the blocking operation completes, only the top-most critical section is
reacquired first

This means you cannot rely on nested critical sections to lock multiple objects
at once, as the inner critical section may suspend the outer ones. Instead, use
:c:macro:`Py_BEGIN_CRITICAL_SECTION2` to lock two objects simultaneously.

Note that the locks described above are only :c:type:`PyMutex` based locks.
The critical section implementation does not know about or affect other locking
mechanisms that might be in use, like POSIX mutexes. Also note that while
blocking on any :c:type:`PyMutex` causes the critical sections to be
suspended, only the mutexes that are part of the critical sections are
released. If :c:type:`PyMutex` is used without a critical section, it will
not be released and therefore does not get the same deadlock avoidance.

Important Considerations
........................

* Critical sections may temporarily release their locks, allowing other threads
to modify the protected data. Be careful about making assumptions about the
state of the data after operations that might block.

* Because locks can be temporarily released (suspended), entering a critical
section does not guarantee exclusive access to the protected resource
throughout the section's duration. If code within a critical section calls
another function that blocks (e.g., acquires another lock, performs blocking
I/O), all locks held by the thread via critical sections will be released.
This is similar to how the GIL can be released during blocking calls.

* Only the lock(s) associated with the most recently entered (top-most)
critical section are guaranteed to be held at any given time. Locks for
outer, nested critical sections might have been suspended.

* You can lock at most two objects simultaneously with these APIs. If you need
to lock more objects, you'll need to restructure your code.

* While critical sections will not deadlock if you attempt to lock the same
object twice, they are less efficient than purpose-built reentrant locks for
this use case.

* When using :c:macro:`Py_BEGIN_CRITICAL_SECTION2`, the order of the objects
doesn't affect correctness (the implementation handles deadlock avoidance),
but it's good practice to always lock objects in a consistent order.

* Remember that the critical section macros are primarily for protecting access
to *Python objects* that might be involved in internal CPython operations
susceptible to the deadlock scenarios described above. For protecting purely
internal extension state, standard mutexes or other synchronization
primitives might be more appropriate.

Platform-specific considerations
................................

On some platforms, Python will look for and load shared library files named
with the ``abi3`` or ``abi3t`` tag (for example, ``mymodule.abi3.so``).
:term:`Free-threaded <free-threaded build>` interpreters only recognize the
``abi3t`` tag, while non-free-threaded ones will prefer ``abi3`` but fall back
to ``abi3t``.
with the ``abi3`` or ``abi3t`` tag (for example, ``mymodule.abi3t.so``).
:term:`Free-threaded <free-threaded build>` interpreters prefer ``abi3t``,
but can fall back to ``abi3``.
Thus, extensions compatible with both ABIs should use the ``abi3t`` tag.

Python does not necessarily check that extensions it loads
Expand All @@ -365,6 +143,7 @@ If you use
`setuptools <https://setuptools.pypa.io/en/latest/setuptools.html>`_ to build
your extension, a future version of ``setuptools`` will allow ``py_limited_api=True``
to be set to allow targeting limited API when building with the free-threaded build.
``uv`` supports targeting PEP 803 as of 0.11.3: `https://github.com/astral-sh/uv/releases/tag/0.11.3`.

`Other build tools will support this ABI as well <https://packaging.python.org/en/latest/guides/tool-recommendations/#build-backends-for-extension-modules>`_.

Expand Down