https://github.com/python/cpython/commit/51086d5e9fda62bddeeb6c8c97a7765e3ea6f52d commit: 51086d5e9fda62bddeeb6c8c97a7765e3ea6f52d branch: 3.15 author: Miss Islington (bot) <[email protected]> committer: hugovk <[email protected]> date: 2026-09-28T10:38:26+03:00 summary:
[3.15] gh-158254: Detect keys changing during reverse dict iteration (GH-158255) (#158267) 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 <[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 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--; _______________________________________________ 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]
