]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.14] gh-142518: add thread safety docs for dict and set APIs (#148392)
authorKumar Aditya <kumaraditya@python.org>
Sat, 11 Apr 2026 16:22:38 +0000 (21:52 +0530)
committerGitHub <noreply@github.com>
Sat, 11 Apr 2026 16:22:38 +0000 (21:52 +0530)
Co-authored-by: Lysandros Nikolaou <lisandrosnik@gmail.com>
Co-authored-by: Victor Stinner <vstinner@python.org>
Doc/c-api/dict.rst
Doc/c-api/set.rst
Doc/data/threadsafety.dat

index 9c4428ced41b5a29e4ab487d3eca3857e1e77379..1629e544d55fc273559b4d76531881ee16870fea 100644 (file)
@@ -65,6 +65,11 @@ Dictionary Objects
    *key*, return ``1``, otherwise return ``0``.  On error, return ``-1``.
    This is equivalent to the Python expression ``key in p``.
 
+   .. 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_ContainsString(PyObject *p, const char *key)
 
@@ -87,6 +92,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)
 
@@ -102,6 +112,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)
 
@@ -120,6 +135,11 @@ Dictionary Objects
    * If the key is missing, set *\*result* to ``NULL`` and return ``0``.
    * On error, raise an exception and return ``-1``.
 
+   .. 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
 
    See also the :c:func:`PyObject_GetItem` function.
@@ -137,6 +157,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.
@@ -149,6 +176,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`.
+
 
 .. c:function:: PyObject* PyDict_GetItemString(PyObject *p, const char *key)
 
@@ -164,6 +198,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`.
+
 
 .. c:function:: int PyDict_GetItemStringRef(PyObject *p, const char *key, PyObject **result)
 
@@ -184,6 +225,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)
 
@@ -203,6 +252,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
 
 
@@ -220,6 +274,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
 
 
@@ -336,6 +395,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)
 
@@ -345,6 +411,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)
 
@@ -360,6 +433,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
@@ -367,6 +447,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)
@@ -375,6 +462,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)
index b74859dd669c54acf966c50cac021e8952cc92fc..c81087697b498db839b18680ccc29c6b4d715b07 100644 (file)
@@ -92,6 +92,11 @@ the constructor functions work with any iterable Python object.
    actually iterable.  The constructor is also useful for copying a set
    (``c=set(s)``).
 
+   .. note::
+
+      The operation is atomic on :term:`free threading <free-threaded build>`
+      when *iterable* is a :class:`set`, :class:`frozenset` or :class:`dict`.
+
 
 .. c:function:: PyObject* PyFrozenSet_New(PyObject *iterable)
 
@@ -100,6 +105,11 @@ the constructor functions work with any iterable Python object.
    set on success or ``NULL`` on failure.  Raise :exc:`TypeError` if *iterable* is
    not actually iterable.
 
+   .. note::
+
+      The operation is atomic on :term:`free threading <free-threaded build>`
+      when *iterable* is a :class:`set`, :class:`frozenset` or :class:`dict`.
+
 
 The following functions and macros are available for instances of :class:`set`
 or :class:`frozenset` or instances of their subtypes.
@@ -127,6 +137,10 @@ or :class:`frozenset` or instances of their subtypes.
    the *key* is unhashable. Raise :exc:`SystemError` if *anyset* is not a
    :class:`set`, :class:`frozenset`, or an instance of a subtype.
 
+   .. 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 PySet_Add(PyObject *set, PyObject *key)
 
@@ -138,6 +152,12 @@ or :class:`frozenset` or instances of their subtypes.
    :exc:`SystemError` if *set* is not an instance of :class:`set` or its
    subtype.
 
+   .. 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`.
+
+
 
 The following functions are available for instances of :class:`set` or its
 subtypes but not for instances of :class:`frozenset` or its subtypes.
@@ -152,6 +172,11 @@ subtypes but not for instances of :class:`frozenset` or its subtypes.
    temporary frozensets. Raise :exc:`SystemError` if *set* is not an
    instance of :class:`set` or its subtype.
 
+   .. 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:: PyObject* PySet_Pop(PyObject *set)
 
@@ -167,6 +192,12 @@ subtypes but not for instances of :class:`frozenset` or its subtypes.
    success. Return ``-1`` and raise :exc:`SystemError` if *set* is not an instance of
    :class:`set` or its subtype.
 
+   .. note::
+
+      In the :term:`free-threaded build`, the set is emptied before its entries
+      are cleared, so other threads will observe an empty set rather than
+      intermediate states.
+
 
 Deprecated API
 ^^^^^^^^^^^^^^
index 82edd1167ef1282786cf2424ee6a8b0886b9aebb..7f9110620db4f37698a952bf641178053132ae58 100644 (file)
 # 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
@@ -125,6 +186,29 @@ PyByteArray_GET_SIZE:atomic:
 PyByteArray_AsString:compatible:
 PyByteArray_AS_STRING:compatible:
 
+# Creation - may iterate the iterable argument, calling arbitrary code.
+# Atomic for sets, frozensets, dicts, and frozendicts.
+PySet_New:shared:
+PyFrozenSet_New:shared:
+
+# Size - uses atomic load on free-threaded builds
+PySet_Size:atomic:
+PySet_GET_SIZE:atomic:
+
+# Contains - lock-free, atomic with simple types
+PySet_Contains:shared:
+
+# Mutations - hold per-object lock for duration
+# atomic with simple types
+PySet_Add:shared:
+PySet_Discard:shared:
+
+# Pop - hold per-object lock for duration
+PySet_Pop:atomic:
+
+# Clear - empties the set before clearing
+PySet_Clear:atomic:
+
 # Capsule objects (Doc/c-api/capsule.rst)
 
 # Type check - read ob_type pointer, always safe