From d8175562b07085104a6e29f087e1793b73ff364e Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Wed, 14 Jan 2026 15:08:28 +0100 Subject: [PATCH 1/2] Add dict thread safety docs --- Doc/library/stdtypes.rst | 109 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index ce0d7cbb2e4276..14286544405d6c 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -5508,6 +5508,115 @@ can be used interchangeably to index the same dictionary entry. .. versionchanged:: 3.8 Dictionaries are now reversible. +.. admonition:: Thread safety + + The following operations and function are lock-free and + :term:`atomic `. + + .. code-block:: + :class: good + + d[key] # dict.__getitem__ + d.get(key) # dict.get + key in d # dict.__contains__ + len(d) # dict.__len__ + + These operations may compare keys using :meth:`~object.__eq__`, which can + execute arbitrary Python code. During such comparisons, the dictionary may + be modified by another thread. For built-in types like :class:`str`, + :class:`int`, and :class:`float`, that implement :meth:`~object.__eq__` in C, + the underlying lock is not released during comparisons and this is not a + concern. + + All other operations from here on hold the per-object lock. + + Writing or removing a single item is safe to call from multiple threads + and will not corrupt the dictionary: + + .. code-block:: + :class: good + + d[key] = value # write + del d[key] # delete + d.pop(key) # remove and return + d.popitem() # remove and return last item + d.setdefault(key, v) # insert if missing + + These operations also compare keys, so the same :meth:`~object.__eq__` + considerations as above apply. + + The following operations return new objects and hold the per-object lock + for the duration: + + .. code-block:: + :class: good + + d.copy() # returns a shallow copy of the dictionary + d | other # merges two dicts into a new dict + d.keys() # returns a new dict_keys view object + d.values() # returns a new dict_values view object + d.items() # returns a new dict_items view object + + The :meth:`~dict.clear` method holds the lock for its duration. Other + threads cannot observe elements being removed. + + The following operations lock both dictionaries. For :meth:`~dict.update` + and ``|=``, this applies only when the other operand is a :class:`dict` + that uses the standard dict iterator (but not subclasses that override + iteration). For equality comparison, this applies to :class:`dict` and + its subclasses: + + .. code-block:: + :class: good + + d.update(other_dict) # both locked when other_dict is a dict + d |= other_dict # both locked when other_dict is a dict + d == other_dict # both locked for dict and subclasses + + The equality comparison also compares values using :meth:`~object.__eq__`, + so for non-built-in types the lock may be released during comparison. + + :meth:`~dict.fromkeys` locks both the new dictionary and the iterable + when the iterable is exactly a :class:`dict`, :class:`set`, or + :class:`frozenset` (not subclasses): + + .. code-block:: + :class: good + + dict.fromkeys(a_dict) # locks both + dict.fromkeys(a_set) # locks both + dict.fromkeys(a_frozenset) # locks both + + When updating from a non-dict iterable, only the target dictionary is + locked. The iterable may be concurrently modified by another thread: + + .. code-block:: + :class: maybe + + d.update(iterable) # iterable is not a dict + d |= iterable # iterable is not a dict + dict.fromkeys(iterable) # iterable is not a dict/set/frozenset + + Operations that involve multiple accesses, as well as iteration, are never + atomic: + + .. code-block:: + :class: bad + + # NOT atomic: read-modify-write + d[key] = d[key] + 1 + + # NOT atomic: check-then-act + if key in d: + del d[key] + + # NOT thread-safe: iteration while modifying + for key in d: + process(key) # another thread may modify d + + Consider external synchronization when sharing :class:`dict` instances + across threads. See :ref:`freethreading-python-howto` for more information. + .. seealso:: :class:`types.MappingProxyType` can be used to create a read-only view From 49293fb36c3716aacdf3c0eafb8379124614d976 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Wed, 21 Jan 2026 15:29:24 +0100 Subject: [PATCH 2/2] Address feedback --- Doc/library/stdtypes.rst | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index 14286544405d6c..fcae83dd37b21e 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -5510,6 +5510,12 @@ can be used interchangeably to index the same dictionary entry. .. admonition:: Thread safety + Creating a dictionary with the :class:`dict` constructor is atomic when the + argument to it is a :class:`dict` or a :class:`tuple`. When using the + :meth:`dict.fromkeys` method, dictionary creation is atomic when the + argument is a :class:`dict`, :class:`tuple`, :class:`set` or + :class:`frozenset`. + The following operations and function are lock-free and :term:`atomic `. @@ -5606,14 +5612,39 @@ can be used interchangeably to index the same dictionary entry. # NOT atomic: read-modify-write d[key] = d[key] + 1 - # NOT atomic: check-then-act + # NOT atomic: check-then-act (TOCTOU) if key in d: del d[key] # NOT thread-safe: iteration while modifying - for key in d: + for key, value in d.items(): process(key) # another thread may modify d + To avoid time-of-check to time-of-use (TOCTOU) issues, use atomic + operations or handle exceptions: + + .. code-block:: + :class: good + + # Use pop() with default instead of check-then-delete + d.pop(key, None) + + # Or handle the exception + try: + del d[key] + except KeyError: + pass + + To safely iterate over a dictionary that may be modified by another + thread, iterate over a copy: + + .. code-block:: + :class: good + + # Make a copy to iterate safely + for key, value in d.copy().items(): + process(key) + Consider external synchronization when sharing :class:`dict` instances across threads. See :ref:`freethreading-python-howto` for more information.