]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-142518: Document thread-safety guarantees of set objects (#145225)
authorLysandros Nikolaou <lisandrosnik@gmail.com>
Fri, 13 Mar 2026 13:53:01 +0000 (14:53 +0100)
committerGitHub <noreply@github.com>
Fri, 13 Mar 2026 13:53:01 +0000 (14:53 +0100)
Doc/library/stdtypes.rst
Doc/library/threadsafety.rst

index 6b55daa9b6eae0c2d63abca13fdba9bca8bb25e7..7ae399eb95ba6e102ac138fba4483736f8edec0e 100644 (file)
@@ -5251,6 +5251,11 @@ Note, the *elem* argument to the :meth:`~object.__contains__`,
 :meth:`~set.discard` methods may be a set.  To support searching for an equivalent
 frozenset, a temporary one is created from *elem*.
 
+.. seealso::
+
+   For detailed information on thread-safety guarantees for :class:`set`
+   objects, see :ref:`thread-safety-set`.
+
 
 .. _typesmapping:
 
index 7ab5921c7ec29840832e2b5132141f295c6385f5..4f2eda19b85e078b176380c155b6f8de52080364 100644 (file)
@@ -342,3 +342,108 @@ thread, iterate over a copy:
 
 Consider external synchronization when sharing :class:`dict` instances
 across threads.
+
+
+.. _thread-safety-set:
+
+Thread safety for set objects
+==============================
+
+The :func:`len` function is lock-free and :term:`atomic <atomic operation>`.
+
+The following read operation is lock-free. It does not block concurrent
+modifications and may observe intermediate states from operations that
+hold the per-object lock:
+
+.. code-block::
+   :class: good
+
+   elem in s    # set.__contains__
+
+This operation may compare elements using :meth:`~object.__eq__`, which can
+execute arbitrary Python code. During such comparisons, the set may be
+modified by another thread. For built-in types like :class:`str`,
+:class:`int`, and :class:`float`, :meth:`!__eq__` does not release the
+underlying lock during comparisons and this is not a concern.
+
+All other operations from here on hold the per-object lock.
+
+Adding or removing a single element is safe to call from multiple threads
+and will not corrupt the set:
+
+.. code-block::
+   :class: good
+
+   s.add(elem)      # add element
+   s.remove(elem)   # remove element, raise if missing
+   s.discard(elem)  # remove element if present
+   s.pop()          # remove and return arbitrary element
+
+These operations also compare elements, so the same :meth:`~object.__eq__`
+considerations as above apply.
+
+The :meth:`~set.copy` method returns a new object and holds the per-object lock
+for the duration so that it is always atomic.
+
+The :meth:`~set.clear` method holds the lock for its duration. Other
+threads cannot observe elements being removed.
+
+The following operations only accept :class:`set` or :class:`frozenset`
+as operands and always lock both objects:
+
+.. code-block::
+   :class: good
+
+   s |= other                   # other must be set/frozenset
+   s &= other                   # other must be set/frozenset
+   s -= other                   # other must be set/frozenset
+   s ^= other                   # other must be set/frozenset
+   s & other                    # other must be set/frozenset
+   s | other                    # other must be set/frozenset
+   s - other                    # other must be set/frozenset
+   s ^ other                    # other must be set/frozenset
+
+:meth:`set.update`, :meth:`set.union`, :meth:`set.intersection` and
+:meth:`set.difference` can take multiple iterables as arguments. They all
+iterate through all the passed iterables and do the following:
+
+   * :meth:`set.update` and :meth:`set.union` lock both objects only when
+      the other operand is a :class:`set`, :class:`frozenset`, or :class:`dict`.
+   * :meth:`set.intersection` and :meth:`set.difference` always try to lock
+      all objects.
+
+:meth:`set.symmetric_difference` tries to lock both objects.
+
+The update variants of the above methods also have some differences between
+them:
+
+   * :meth:`set.difference_update` and :meth:`set.intersection_update` try
+      to lock all objects one-by-one.
+   * :meth:`set.symmetric_difference_update` only locks the arguments if it is
+      of type :class:`set`, :class:`frozenset`, or :class:`dict`.
+
+The following methods always try to lock both objects:
+
+.. code-block::
+   :class: good
+
+   s.isdisjoint(other)          # both locked
+   s.issubset(other)            # both locked
+   s.issuperset(other)          # both locked
+
+Operations that involve multiple accesses, as well as iteration, are never
+atomic:
+
+.. code-block::
+   :class: bad
+
+   # NOT atomic: check-then-act
+   if elem in s:
+         s.remove(elem)
+
+   # NOT thread-safe: iteration while modifying
+   for elem in s:
+         process(elem)  # another thread may modify s
+
+Consider external synchronization when sharing :class:`set` instances
+across threads.  See :ref:`freethreading-python-howto` for more information.