From ab052b9a84cbadd622a94415bbbe88af141f2f71 Mon Sep 17 00:00:00 2001 From: Joshua Bronson Date: Sat, 26 Sep 2026 22:52:51 -0400 Subject: [PATCH] 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". (cherry picked from commit 042a7c150552a4ddb5a90cf65a6e5b9e7ba3f7dc) Co-authored-by: Joshua Bronson Co-authored-by: Claude Opus 5.5 --- Lib/test/test_dict.py | 25 +++++++++++++++++++ ...-09-26-15-39-32.gh-issue-158254.qT7vRk.rst | 3 +++ Objects/dictobject.c | 6 +++++ 3 files changed, 34 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-09-26-15-39-32.gh-issue-158254.qT7vRk.rst diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py index 1e665c86303078..7035e72cdda09c 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 53dc90be4c91e2..b31cb8f14ebc70 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -6254,6 +6254,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--;