https://github.com/python/cpython/commit/f9589cb1b26c1f9d7cd0fe823ba211ea6c5804cd
commit: f9589cb1b26c1f9d7cd0fe823ba211ea6c5804cd
branch: 3.14
author: Miss Islington (bot) <[email protected]>
committer: hugovk <[email protected]>
date: 2026-03-12T15:36:58+02:00
summary:

[3.14] gh-145254: Add thread safety annotation in docs (GH-145255) (#145862)

Co-authored-by: Lysandros Nikolaou <[email protected]>

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 a6819d4af26440..c0e26f4f7e1458 100644
--- a/Doc/conf.py
+++ b/Doc/conf.py
@@ -566,6 +566,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]

Reply via email to