https://github.com/python/cpython/commit/f0851768630e1df8f18326621eeb8ce9b442d03c
commit: f0851768630e1df8f18326621eeb8ce9b442d03c
branch: 3.14
author: Miss Islington (bot) <[email protected]>
committer: ZeroIntensity <[email protected]>
date: 2026-09-27T03:21:33Z
summary:

[3.14] gh-158254: Detect keys changing during reverse dict iteration 
(GH-158255) (GH-158268)

A reverse dict iterator only checked that the dict's size was unchanged,
so after the keys were replaced with the same number of keys it kept
yielding entries for the new keys. It could yield more items than the
dict had when it was created, and __length_hint__() then wrapped around.

Add the check the forward iterators already have: finding an entry after
the expected number of items raises "dictionary keys changed during
iteration".
(cherry picked from commit 042a7c150552a4ddb5a90cf65a6e5b9e7ba3f7dc)

Co-authored-by: Joshua Bronson <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-09-26-15-39-32.gh-issue-158254.qT7vRk.rst
M Lib/test/test_dict.py
M Objects/dictobject.c

diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py
index 8f8ebc15febf62d..47e98c40583ac84 100644
--- a/Lib/test/test_dict.py
+++ b/Lib/test/test_dict.py
@@ -1424,6 +1424,31 @@ def test_reversed_dict_after_clear_and_restore(self):
         for it in iterators:
             self.assertEqual(list(it), [])
 
+    def test_reversed_dict_keys_changed_during_iteration(self):
+        d = dict.fromkeys(range(10))
+        for i in range(7):
+            del d[i]
+
+        iterators = (
+            reversed(d),
+            reversed(d.keys()),
+            reversed(d.values()),
+            reversed(d.items()),
+        )
+        for it in iterators:
+            next(it)
+
+        # Same size as before, but with different keys below
+        # the iterators' current position.
+        d.clear()
+        d.update(dict.fromkeys(range(10)))
+        for i in range(3, 10):
+            del d[i]
+
+        for it in iterators:
+            with self.assertRaisesRegex(RuntimeError, 'keys changed'):
+                list(it)
+
     def test_dict_copy_order(self):
         # bpo-34320
         od = collections.OrderedDict([('a', 1), ('b', 2)])
diff --git 
a/Misc/NEWS.d/next/Core_and_Builtins/2026-09-26-15-39-32.gh-issue-158254.qT7vRk.rst
 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-26-15-39-32.gh-issue-158254.qT7vRk.rst
new file mode 100644
index 000000000000000..21908eea4a8236b
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-26-15-39-32.gh-issue-158254.qT7vRk.rst
@@ -0,0 +1,3 @@
+Reverse iterators over a :class:`dict` and its views now raise
+:exc:`RuntimeError` if the dictionary's keys change during iteration, like
+forward iterators, instead of yielding entries for the new keys.
diff --git a/Objects/dictobject.c b/Objects/dictobject.c
index c7023e446c4b028..e285f44ca92ddfb 100644
--- a/Objects/dictobject.c
+++ b/Objects/dictobject.c
@@ -5828,6 +5828,12 @@ dictreviter_iter_lock_held(PyDictObject *d, PyObject 
*self)
             value = entry_ptr->me_value;
         }
     }
+    // We found an element, but did not expect it
+    if (di->len == 0) {
+        PyErr_SetString(PyExc_RuntimeError,
+                        "dictionary keys changed during iteration");
+        goto fail;
+    }
     di->di_pos = i-1;
     di->len--;
 

_______________________________________________
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]

Reply via email to