https://github.com/python/cpython/commit/15d10a82439e2a83cb004dd16786b168e82cba2e
commit: 15d10a82439e2a83cb004dd16786b168e82cba2e
branch: main
author: Lysandros Nikolaou <[email protected]>
committer: kumaraditya303 <[email protected]>
date: 2026-04-10T18:41:12Z
summary:
gh-142518: annotate dict C-APIs for thread safety (#145875)
files:
M Doc/c-api/dict.rst
M Doc/data/threadsafety.dat
diff --git a/Doc/c-api/dict.rst b/Doc/c-api/dict.rst
index 7a925870a62a57..a2a0d0d80657eb 100644
--- a/Doc/c-api/dict.rst
+++ b/Doc/c-api/dict.rst
@@ -76,6 +76,11 @@ Dictionary objects
The first argument can be a :class:`dict` or a :class:`frozendict`.
+ .. note::
+
+ The operation is atomic on :term:`free threading <free-threaded build>`
+ when *key* is :class:`str`, :class:`int`, :class:`float`, :class:`bool`
or :class:`bytes`.
+
.. versionchanged:: 3.15
Also accept :class:`frozendict`.
@@ -105,6 +110,11 @@ Dictionary objects
``0`` on success or ``-1`` on failure. This function *does not* steal a
reference to *val*.
+ .. note::
+
+ The operation is atomic on :term:`free threading <free-threaded build>`
+ when *key* is :class:`str`, :class:`int`, :class:`float`, :class:`bool`
or :class:`bytes`.
+
.. c:function:: int PyDict_SetItemString(PyObject *p, const char *key,
PyObject *val)
@@ -120,6 +130,11 @@ Dictionary objects
If *key* is not in the dictionary, :exc:`KeyError` is raised.
Return ``0`` on success or ``-1`` on failure.
+ .. note::
+
+ The operation is atomic on :term:`free threading <free-threaded build>`
+ when *key* is :class:`str`, :class:`int`, :class:`float`, :class:`bool`
or :class:`bytes`.
+
.. c:function:: int PyDict_DelItemString(PyObject *p, const char *key)
@@ -140,6 +155,11 @@ Dictionary objects
The first argument can be a :class:`dict` or a :class:`frozendict`.
+ .. note::
+
+ The operation is atomic on :term:`free threading <free-threaded build>`
+ when *key* is :class:`str`, :class:`int`, :class:`float`, :class:`bool`
or :class:`bytes`.
+
.. versionadded:: 3.13
.. versionchanged:: 3.15
@@ -162,6 +182,13 @@ Dictionary objects
:meth:`~object.__eq__` methods are silently ignored.
Prefer the :c:func:`PyDict_GetItemWithError` function instead.
+ .. note::
+
+ In the :term:`free-threaded build`, the returned
+ :term:`borrowed reference` may become invalid if another thread modifies
+ the dictionary concurrently. Prefer :c:func:`PyDict_GetItemRef`, which
+ returns a :term:`strong reference`.
+
.. versionchanged:: 3.10
Calling this API without an :term:`attached thread state` had been
allowed for historical
reason. It is no longer allowed.
@@ -177,6 +204,13 @@ Dictionary objects
occurred. Return ``NULL`` **without** an exception set if the key
wasn't present.
+ .. note::
+
+ In the :term:`free-threaded build`, the returned
+ :term:`borrowed reference` may become invalid if another thread modifies
+ the dictionary concurrently. Prefer :c:func:`PyDict_GetItemRef`, which
+ returns a :term:`strong reference`.
+
.. versionchanged:: 3.15
Also accept :class:`frozendict`.
@@ -195,6 +229,13 @@ Dictionary objects
Prefer using the :c:func:`PyDict_GetItemWithError` function with your own
:c:func:`PyUnicode_FromString` *key* instead.
+ .. note::
+
+ In the :term:`free-threaded build`, the returned
+ :term:`borrowed reference` may become invalid if another thread modifies
+ the dictionary concurrently. Prefer :c:func:`PyDict_GetItemStringRef`,
+ which returns a :term:`strong reference`.
+
.. versionchanged:: 3.15
Also accept :class:`frozendict`.
@@ -221,6 +262,14 @@ Dictionary objects
.. versionadded:: 3.4
+ .. note::
+
+ In the :term:`free-threaded build`, the returned
+ :term:`borrowed reference` may become invalid if another thread modifies
+ the dictionary concurrently. Prefer :c:func:`PyDict_SetDefaultRef`,
+ which returns a :term:`strong reference`.
+
+
.. c:function:: int PyDict_SetDefaultRef(PyObject *p, PyObject *key, PyObject
*default_value, PyObject **result)
@@ -240,6 +289,11 @@ Dictionary objects
These may refer to the same object: in that case you hold two separate
references to it.
+ .. note::
+
+ The operation is atomic on :term:`free threading <free-threaded build>`
+ when *key* is :class:`str`, :class:`int`, :class:`float`, :class:`bool`
or :class:`bytes`.
+
.. versionadded:: 3.13
@@ -257,6 +311,11 @@ Dictionary objects
Similar to :meth:`dict.pop`, but without the default value and
not raising :exc:`KeyError` if the key is missing.
+ .. note::
+
+ The operation is atomic on :term:`free threading <free-threaded build>`
+ when *key* is :class:`str`, :class:`int`, :class:`float`, :class:`bool`
or :class:`bytes`.
+
.. versionadded:: 3.13
@@ -403,6 +462,13 @@ Dictionary objects
only be added if there is not a matching key in *a*. Return ``0`` on
success or ``-1`` if an exception was raised.
+ .. note::
+
+ In the :term:`free-threaded build`, when *b* is a
+ :class:`dict` (with the standard iterator), both *a* and *b* are locked
+ for the duration of the operation. When *b* is a non-dict mapping, only
+ *a* is locked; *b* may be concurrently modified by another thread.
+
.. c:function:: int PyDict_Update(PyObject *a, PyObject *b)
@@ -412,6 +478,13 @@ Dictionary objects
argument has no "keys" attribute. Return ``0`` on success or ``-1`` if an
exception was raised.
+ .. note::
+
+ In the :term:`free-threaded build`, when *b* is a
+ :class:`dict` (with the standard iterator), both *a* and *b* are locked
+ for the duration of the operation. When *b* is a non-dict mapping, only
+ *a* is locked; *b* may be concurrently modified by another thread.
+
.. c:function:: int PyDict_MergeFromSeq2(PyObject *a, PyObject *seq2, int
override)
@@ -427,6 +500,13 @@ Dictionary objects
if override or key not in a:
a[key] = value
+ .. note::
+
+ In the :term:`free-threaded <free threading>` build, only *a* is locked.
+ The iteration over *seq2* is not synchronized; *seq2* may be concurrently
+ modified by another thread.
+
+
.. c:function:: int PyDict_AddWatcher(PyDict_WatchCallback callback)
Register *callback* as a dictionary watcher. Return a non-negative integer
@@ -434,6 +514,13 @@ Dictionary objects
of error (e.g. no more watcher IDs available), return ``-1`` and set an
exception.
+ .. note::
+
+ This function is not internally synchronized. In the
+ :term:`free-threaded <free threading>` build, callers should ensure no
+ concurrent calls to :c:func:`PyDict_AddWatcher` or
+ :c:func:`PyDict_ClearWatcher` are in progress.
+
.. versionadded:: 3.12
.. c:function:: int PyDict_ClearWatcher(int watcher_id)
@@ -442,6 +529,13 @@ Dictionary objects
:c:func:`PyDict_AddWatcher`. Return ``0`` on success, ``-1`` on error (e.g.
if the given *watcher_id* was never registered.)
+ .. note::
+
+ This function is not internally synchronized. In the
+ :term:`free-threaded <free threading>` build, callers should ensure no
+ concurrent calls to :c:func:`PyDict_AddWatcher` or
+ :c:func:`PyDict_ClearWatcher` are in progress.
+
.. versionadded:: 3.12
.. c:function:: int PyDict_Watch(int watcher_id, PyObject *dict)
diff --git a/Doc/data/threadsafety.dat b/Doc/data/threadsafety.dat
index 7c381ecb70c836..d5432348e10638 100644
--- a/Doc/data/threadsafety.dat
+++ b/Doc/data/threadsafety.dat
@@ -14,10 +14,71 @@
# 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_Lock:atomic:
+PyMutex_Unlock:atomic:
PyMutex_IsLocked:atomic:
+
+# Dictionary objects (Doc/c-api/dict.rst)
+
+# Type checks - read ob_type pointer, always safe
+PyDict_Check:atomic:
+PyDict_CheckExact:atomic:
+
+# Creation - pure allocation, no shared state
+PyDict_New:atomic:
+
+# Lock-free lookups - use _Py_dict_lookup_threadsafe(), no locking.
+# Atomic with simple types.
+PyDict_Contains:shared:
+PyDict_ContainsString:atomic:
+PyDict_GetItemRef:shared:
+PyDict_GetItemStringRef:atomic:
+PyDict_Size:atomic:
+PyDict_GET_SIZE:atomic:
+
+# Borrowed-reference lookups - lock-free dict access but returned
+# borrowed reference is unsafe in free-threaded builds without
+# external synchronization
+PyDict_GetItem:compatible:
+PyDict_GetItemWithError:compatible:
+PyDict_GetItemString:compatible:
+PyDict_SetDefault:compatible:
+
+# Iteration - no locking; returns borrowed refs
+PyDict_Next:compatible:
+
+# Single-item mutations - protected by per-object critical section
+PyDict_SetItem:shared:
+PyDict_SetItemString:atomic:
+PyDict_DelItem:shared:
+PyDict_DelItemString:atomic:
+PyDict_SetDefaultRef:shared:
+PyDict_Pop:shared:
+PyDict_PopString:atomic:
+
+# Bulk reads - hold per-object lock for duration
+PyDict_Clear:atomic:
+PyDict_Copy:atomic:
+PyDict_Keys:atomic:
+PyDict_Values:atomic:
+PyDict_Items:atomic:
+
+# Merge/update - lock target dict; also lock source when it is a dict
+PyDict_Update:shared:
+PyDict_Merge:shared:
+PyDict_MergeFromSeq2:shared:
+
+# Watcher registration - no synchronization on interpreter state
+PyDict_AddWatcher:compatible:
+PyDict_ClearWatcher:compatible:
+
+# Per-dict watcher tags - non-atomic RMW on _ma_watcher_tag;
+# safe on distinct dicts only
+PyDict_Watch:distinct:
+PyDict_Unwatch:distinct:
+
+
# List objects (Doc/c-api/list.rst)
# Type checks - read ob_type pointer, always safe
_______________________________________________
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]