https://github.com/python/cpython/commit/0dce4c6eab9dcfcb44bb2739dc60c8ac5fa1ebad
commit: 0dce4c6eab9dcfcb44bb2739dc60c8ac5fa1ebad
branch: main
author: Lysandros Nikolaou <[email protected]>
committer: kumaraditya303 <[email protected]>
date: 2026-03-12T12:18:43+05:30
summary:
gh-145254: Add thread safety annotation in docs (#145255)
files:
A Doc/data/threadsafety.dat
M Doc/conf.py
M Doc/library/threadsafety.rst
M Doc/tools/extensions/c_annotations.py
diff --git a/Doc/conf.py b/Doc/conf.py
index 6f66ed68c52e83..4ac6f6192a0806 100644
--- a/Doc/conf.py
+++ b/Doc/conf.py
@@ -569,6 +569,7 @@
# Relative filename of the data files
refcount_file = 'data/refcounts.dat'
stable_abi_file = 'data/stable_abi.dat'
+threadsafety_file = 'data/threadsafety.dat'
# Options for sphinxext-opengraph
# -------------------------------
diff --git a/Doc/data/threadsafety.dat b/Doc/data/threadsafety.dat
new file mode 100644
index 00000000000000..f063ca1360d5fb
--- /dev/null
+++ b/Doc/data/threadsafety.dat
@@ -0,0 +1,19 @@
+# Thread safety annotations for C API functions.
+#
+# Each line has the form:
+# function_name : level
+#
+# Where level is one of:
+# incompatible -- not safe even with external locking
+# compatible -- safe if the caller serializes all access with external
locks
+# distinct -- safe on distinct objects without external synchronization
+# shared -- safe for concurrent use on the same object
+# atomic -- atomic
+#
+# Lines beginning with '#' are ignored.
+# The function name must match the C domain identifier used in the
documentation.
+
+# Synchronization primitives (Doc/c-api/synchronization.rst)
+PyMutex_Lock:shared:
+PyMutex_Unlock:shared:
+PyMutex_IsLocked:atomic:
diff --git a/Doc/library/threadsafety.rst b/Doc/library/threadsafety.rst
index 5b5949d4eff437..7ab5921c7ec298 100644
--- a/Doc/library/threadsafety.rst
+++ b/Doc/library/threadsafety.rst
@@ -13,6 +13,88 @@ For general guidance on writing thread-safe code in
free-threaded Python, see
:ref:`freethreading-python-howto`.
+.. _threadsafety-levels:
+
+Thread safety levels
+====================
+
+The C API documentation uses the following levels to describe the thread
+safety guarantees of each function. The levels are listed from least to
+most safe.
+
+.. _threadsafety-level-incompatible:
+
+Incompatible
+------------
+
+A function or operation that cannot be made safe for concurrent use even
+with external synchronization. Incompatible code typically accesses
+global state in an unsynchronized way and must only be called from a single
+thread throughout the program's lifetime.
+
+Example: a function that modifies process-wide state such as signal handlers
+or environment variables, where concurrent calls from any threads, even with
+external locking, can conflict with the runtime or other libraries.
+
+.. _threadsafety-level-compatible:
+
+Compatible
+----------
+
+A function or operation that is safe to call from multiple threads
+*provided* the caller supplies appropriate external synchronization, for
+example by holding a :term:`lock` for the duration of each call. Without
+such synchronization, concurrent calls may produce :term:`race conditions
+<race condition>` or :term:`data races <data race>`.
+
+Example: a function that reads from or writes to an object whose internal
+state is not protected by a lock. Callers must ensure that no two threads
+access the same object at the same time.
+
+.. _threadsafety-level-distinct:
+
+Safe on distinct objects
+------------------------
+
+A function or operation that is safe to call from multiple threads without
+external synchronization, as long as each thread operates on a **different**
+object. Two threads may call the function at the same time, but they must
+not pass the same object (or objects that share underlying state) as
+arguments.
+
+Example: a function that modifies fields of a struct using non-atomic
+writes. Two threads can each call the function on their own struct
+instance safely, but concurrent calls on the *same* instance require
+external synchronization.
+
+.. _threadsafety-level-shared:
+
+Safe on shared objects
+----------------------
+
+A function or operation that is safe for concurrent use on the **same**
+object. The implementation uses internal synchronization (such as
+:term:`per-object locks <per-object lock>` or
+:ref:`critical sections <python-critical-section-api>`) to protect shared
+mutable state, so callers do not need to supply their own locking.
+
+Example: :c:func:`PyList_GetItemRef` can be called from multiple threads on the
+same :c:type:`PyListObject` - it uses internal synchronization to serialize
+access.
+
+.. _threadsafety-level-atomic:
+
+Atomic
+------
+
+A function or operation that appears :term:`atomic <atomic operation>` with
+respect to other threads - it executes instantaneously from the perspective
+of other threads. This is the strongest form of thread safety.
+
+Example: :c:func:`PyMutex_IsLocked` performs an atomic read of the mutex
+state and can be called from any thread at any time.
+
+
.. _thread-safety-list:
Thread safety for list objects
diff --git a/Doc/tools/extensions/c_annotations.py
b/Doc/tools/extensions/c_annotations.py
index e04a5f144c449b..58f597c2eb2d0c 100644
--- a/Doc/tools/extensions/c_annotations.py
+++ b/Doc/tools/extensions/c_annotations.py
@@ -3,10 +3,12 @@
* Reference count annotations for C API functions.
* Stable ABI annotations
* Limited API annotations
+* Thread safety annotations for C API functions.
Configuration:
* Set ``refcount_file`` to the path to the reference count data file.
* Set ``stable_abi_file`` to the path to stable ABI list.
+* Set ``threadsafety_file`` to the path to the thread safety data file.
"""
from __future__ import annotations
@@ -48,6 +50,15 @@ class RefCountEntry:
result_refs: int | None = None
[email protected](frozen=True, slots=True)
+class ThreadSafetyEntry:
+ # Name of the function.
+ name: str
+ # Thread safety level.
+ # One of: 'incompatible', 'compatible', 'safe'.
+ level: str
+
+
@dataclasses.dataclass(frozen=True, slots=True)
class StableABIEntry:
# Role of the object.
@@ -113,10 +124,42 @@ def read_stable_abi_data(stable_abi_file: Path) ->
dict[str, StableABIEntry]:
return stable_abi_data
+_VALID_THREADSAFETY_LEVELS = frozenset({
+ "incompatible",
+ "compatible",
+ "distinct",
+ "shared",
+ "atomic",
+})
+
+
+def read_threadsafety_data(
+ threadsafety_filename: Path,
+) -> dict[str, ThreadSafetyEntry]:
+ threadsafety_data = {}
+ for line in threadsafety_filename.read_text(encoding="utf8").splitlines():
+ line = line.strip()
+ if not line or line.startswith("#"):
+ continue
+ # Each line is of the form: function_name : level : [comment]
+ parts = line.split(":", 2)
+ if len(parts) < 2:
+ raise ValueError(f"Wrong field count in {line!r}")
+ name, level = parts[0].strip(), parts[1].strip()
+ if level not in _VALID_THREADSAFETY_LEVELS:
+ raise ValueError(
+ f"Unknown thread safety level {level!r} for {name!r}. "
+ f"Valid levels: {sorted(_VALID_THREADSAFETY_LEVELS)}"
+ )
+ threadsafety_data[name] = ThreadSafetyEntry(name=name, level=level)
+ return threadsafety_data
+
+
def add_annotations(app: Sphinx, doctree: nodes.document) -> None:
state = app.env.domaindata["c_annotations"]
refcount_data = state["refcount_data"]
stable_abi_data = state["stable_abi_data"]
+ threadsafety_data = state["threadsafety_data"]
for node in doctree.findall(addnodes.desc_content):
par = node.parent
if par["domain"] != "c":
@@ -126,6 +169,12 @@ def add_annotations(app: Sphinx, doctree: nodes.document)
-> None:
name = par[0]["ids"][0].removeprefix("c.")
objtype = par["objtype"]
+ # Thread safety annotation — inserted first so it appears last
(bottom-most)
+ # among all annotations.
+ if entry := threadsafety_data.get(name):
+ annotation = _threadsafety_annotation(entry.level)
+ node.insert(0, annotation)
+
# Stable ABI annotation.
if record := stable_abi_data.get(name):
if ROLE_TO_OBJECT_TYPE[record.role] != objtype:
@@ -256,6 +305,46 @@ def _unstable_api_annotation() -> nodes.admonition:
)
+def _threadsafety_annotation(level: str) -> nodes.emphasis:
+ match level:
+ case "incompatible":
+ display = sphinx_gettext("Not safe to call from multiple threads.")
+ reftarget = "threadsafety-level-incompatible"
+ case "compatible":
+ display = sphinx_gettext(
+ "Safe to call from multiple threads"
+ " with external synchronization only."
+ )
+ reftarget = "threadsafety-level-compatible"
+ case "distinct":
+ display = sphinx_gettext(
+ "Safe to call without external synchronization"
+ " on distinct objects."
+ )
+ reftarget = "threadsafety-level-distinct"
+ case "shared":
+ display = sphinx_gettext(
+ "Safe for concurrent use on the same object."
+ )
+ reftarget = "threadsafety-level-shared"
+ case "atomic":
+ display = sphinx_gettext("Atomic.")
+ reftarget = "threadsafety-level-atomic"
+ case _:
+ raise AssertionError(f"Unknown thread safety level {level!r}")
+ ref_node = addnodes.pending_xref(
+ display,
+ nodes.Text(display),
+ refdomain="std",
+ reftarget=reftarget,
+ reftype="ref",
+ refexplicit="True",
+ )
+ prefix = sphinx_gettext("Thread safety:") + " "
+ classes = ["threadsafety", f"threadsafety-{level}"]
+ return nodes.emphasis("", prefix, ref_node, classes=classes)
+
+
def _return_value_annotation(result_refs: int | None) -> nodes.emphasis:
classes = ["refcount"]
if result_refs is None:
@@ -342,11 +431,15 @@ def init_annotations(app: Sphinx) -> None:
state["stable_abi_data"] = read_stable_abi_data(
Path(app.srcdir, app.config.stable_abi_file)
)
+ state["threadsafety_data"] = read_threadsafety_data(
+ Path(app.srcdir, app.config.threadsafety_file)
+ )
def setup(app: Sphinx) -> ExtensionMetadata:
app.add_config_value("refcount_file", "", "env", types={str})
app.add_config_value("stable_abi_file", "", "env", types={str})
+ app.add_config_value("threadsafety_file", "", "env", types={str})
app.add_directive("limited-api-list", LimitedAPIList)
app.add_directive("corresponding-type-slot", CorrespondingTypeSlot)
app.connect("builder-inited", init_annotations)
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]