https://github.com/python/cpython/commit/042a7c150552a4ddb5a90cf65a6e5b9e7ba3f7dc
commit: 042a7c150552a4ddb5a90cf65a6e5b9e7ba3f7dc
branch: main
author: Joshua Bronson <[email protected]>
committer: ZeroIntensity <[email protected]>
date: 2026-09-26T22:52:51-04:00
summary:

gh-158254: Detect keys changing during reverse dict iteration (GH-158255)

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".

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 673987733fc8c4..3b9ff672a1aa03 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 00000000000000..21908eea4a8236
--- /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 9a469f88230f8b..15377ac083c2b2 100644
--- a/Objects/dictobject.c
+++ b/Objects/dictobject.c
@@ -6303,6 +6303,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