From d8175562b07085104a6e29f087e1793b73ff364e Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Wed, 14 Jan 2026 15:08:28 +0100 Subject: [PATCH 1/7] 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/7] 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. From 90cac54715593019fe7d0373cf246d5689986d3c Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Mon, 26 Jan 2026 13:04:59 +0100 Subject: [PATCH 3/7] Address feedback; move thread safety section below see-also --- Doc/library/stdtypes.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index fcae83dd37b21e..5509ff1c886da3 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -5508,6 +5508,12 @@ can be used interchangeably to index the same dictionary entry. .. versionchanged:: 3.8 Dictionaries are now reversible. + +.. seealso:: + :class:`types.MappingProxyType` can be used to create a read-only view + of a :class:`dict`. + + .. admonition:: Thread safety Creating a dictionary with the :class:`dict` constructor is atomic when the @@ -5649,11 +5655,6 @@ can be used interchangeably to index the same dictionary entry. across threads. See :ref:`freethreading-python-howto` for more information. -.. seealso:: - :class:`types.MappingProxyType` can be used to create a read-only view - of a :class:`dict`. - - .. _dict-views: Dictionary view objects From 4187ff019697bf939c568be420d401412b26fd39 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Fri, 6 Feb 2026 18:28:03 +0100 Subject: [PATCH 4/7] Apply suggestions from code review Co-authored-by: Petr Viktorin --- Doc/library/stdtypes.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index 5509ff1c886da3..db15131deb446e 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -5558,7 +5558,7 @@ can be used interchangeably to index the same dictionary entry. considerations as above apply. The following operations return new objects and hold the per-object lock - for the duration: + for the duration of the operation: .. code-block:: :class: good @@ -5605,9 +5605,9 @@ can be used interchangeably to index the same dictionary entry. .. 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 + d.update(iterable) # iterable is not a dict: only d locked + d |= iterable # iterable is not a dict: only d locked + dict.fromkeys(iterable) # iterable is not a dict/set/frozenset: only result locked Operations that involve multiple accesses, as well as iteration, are never atomic: From 4235edc35b138999007208bd4afb6bae3402b2ab Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Fri, 6 Feb 2026 18:24:29 +0100 Subject: [PATCH 5/7] Remove double spaces and fix typo --- Doc/library/stdtypes.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index db15131deb446e..0574b3b7a1592d 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -5517,12 +5517,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 + 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 + The following operations and functions are lock-free and :term:`atomic `. .. code-block:: @@ -5534,8 +5534,8 @@ can be used interchangeably to index the same dictionary entry. 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`, + 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. @@ -5569,13 +5569,13 @@ can be used interchangeably to index the same dictionary entry. 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 + 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` + 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 + iteration). For equality comparison, this applies to :class:`dict` and its subclasses: .. code-block:: @@ -5600,7 +5600,7 @@ can be used interchangeably to index the same dictionary entry. 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: + locked. The iterable may be concurrently modified by another thread: .. code-block:: :class: maybe @@ -5652,7 +5652,7 @@ can be used interchangeably to index the same dictionary entry. process(key) Consider external synchronization when sharing :class:`dict` instances - across threads. See :ref:`freethreading-python-howto` for more information. + across threads. See :ref:`freethreading-python-howto` for more information. .. _dict-views: From d7782ea53256523d88376349707c0eb99dc8f65c Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Fri, 6 Feb 2026 18:31:02 +0100 Subject: [PATCH 6/7] Address feedback - don't mention equality comparison only --- Doc/library/stdtypes.rst | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index 0574b3b7a1592d..29f0234d0b5f47 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -5533,13 +5533,6 @@ can be used interchangeably to index the same dictionary entry. 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 @@ -5554,8 +5547,12 @@ can be used interchangeably to index the same dictionary entry. 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. + 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. The following operations return new objects and hold the per-object lock for the duration of the operation: @@ -5585,7 +5582,7 @@ can be used interchangeably to index the same dictionary entry. 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__`, + All comparison operations also compare 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 From c889a51f37886552c141c49ad0463b9b703fa70f Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Wed, 11 Feb 2026 14:25:34 +0100 Subject: [PATCH 7/7] Change admonition to rubric; cross-reference glossary --- Doc/library/stdtypes.rst | 202 ++++++++++++++++++++------------------- 1 file changed, 102 insertions(+), 100 deletions(-) diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index 1afc3160d9e6f0..0e5f5dc39e7277 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -5593,142 +5593,144 @@ can be used interchangeably to index the same dictionary entry. of a :class:`dict`. -.. admonition:: Thread safety +.. _thread-safety-dict: - 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`. +.. rubric:: Thread safety for dict objects - The following operations and functions are lock-free and - :term:`atomic `. +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`. - .. code-block:: - :class: good +The following operations and functions are :term:`lock-free` and +:term:`atomic `. - d[key] # dict.__getitem__ - d.get(key) # dict.get - key in d # dict.__contains__ - len(d) # dict.__len__ +.. code-block:: + :class: good - All other operations from here on hold the per-object lock. + d[key] # dict.__getitem__ + d.get(key) # dict.get + key in d # dict.__contains__ + len(d) # dict.__len__ - Writing or removing a single item is safe to call from multiple threads - and will not corrupt the dictionary: +All other operations from here on hold the :term:`per-object lock`. - .. code-block:: - :class: good +Writing or removing a single item is safe to call from multiple threads +and will not corrupt the dictionary: - 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 +.. code-block:: + :class: good - 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. + 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 - The following operations return new objects and hold the per-object lock - for the duration of the operation: +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. - .. code-block:: - :class: good +The following operations return new objects and hold the :term:`per-object lock` +for the duration of the operation: - 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 +.. code-block:: + :class: good - The :meth:`~dict.clear` method holds the lock for its duration. Other - threads cannot observe elements being removed. + 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 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: +The :meth:`~dict.clear` method holds the lock for its duration. Other +threads cannot observe elements being removed. - .. code-block:: - :class: good +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: - 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 +.. 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 - All comparison operations also compare values using :meth:`~object.__eq__`, - so for non-built-in types the lock may be released during comparison. +All comparison operations also compare 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): +: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 +.. code-block:: + :class: good - dict.fromkeys(a_dict) # locks both - dict.fromkeys(a_set) # locks both - dict.fromkeys(a_frozenset) # locks both + 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: +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 +.. code-block:: + :class: maybe - d.update(iterable) # iterable is not a dict: only d locked - d |= iterable # iterable is not a dict: only d locked - dict.fromkeys(iterable) # iterable is not a dict/set/frozenset: only result locked + d.update(iterable) # iterable is not a dict: only d locked + d |= iterable # iterable is not a dict: only d locked + dict.fromkeys(iterable) # iterable is not a dict/set/frozenset: only result locked - Operations that involve multiple accesses, as well as iteration, are never - atomic: +Operations that involve multiple accesses, as well as iteration, are never +atomic: - .. code-block:: - :class: bad +.. code-block:: + :class: bad - # NOT atomic: read-modify-write - d[key] = d[key] + 1 + # NOT atomic: read-modify-write + d[key] = d[key] + 1 - # NOT atomic: check-then-act (TOCTOU) - if key in d: - del d[key] + # NOT atomic: check-then-act (TOCTOU) + if key in d: + del d[key] - # NOT thread-safe: iteration while modifying - for key, value in d.items(): - process(key) # another thread may modify d + # NOT thread-safe: iteration while modifying + 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: +To avoid time-of-check to time-of-use (TOCTOU) issues, use atomic +operations or handle exceptions: - .. code-block:: - :class: good +.. code-block:: + :class: good - # Use pop() with default instead of check-then-delete - d.pop(key, None) + # Use pop() with default instead of check-then-delete + d.pop(key, None) - # Or handle the exception - try: - del d[key] - except KeyError: - pass + # 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: +To safely iterate over a dictionary that may be modified by another +thread, iterate over a copy: - .. code-block:: - :class: good +.. code-block:: + :class: good - # Make a copy to iterate safely - for key, value in d.copy().items(): - process(key) + # 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. +Consider external synchronization when sharing :class:`dict` instances +across threads. See :ref:`freethreading-python-howto` for more information. .. _dict-views: