Skip to content

Commit ff03e45

Browse files
miss-islingtonjabclaude
authored
[3.13] gh-158254: Detect keys changing during reverse dict iteration (GH-158255) (GH-158269)
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 042a7c1) Co-authored-by: Joshua Bronson <jabronson@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 59c249e commit ff03e45

3 files changed

Lines changed: 34 additions & 0 deletions

File tree

‎Lib/test/test_dict.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1517,6 +1517,31 @@ def test_reversed_dict_after_clear_and_restore(self):
15171517
for it in iterators:
15181518
self.assertEqual(list(it), [])
15191519

1520+
def test_reversed_dict_keys_changed_during_iteration(self):
1521+
d = dict.fromkeys(range(10))
1522+
for i in range(7):
1523+
del d[i]
1524+
1525+
iterators = (
1526+
reversed(d),
1527+
reversed(d.keys()),
1528+
reversed(d.values()),
1529+
reversed(d.items()),
1530+
)
1531+
for it in iterators:
1532+
next(it)
1533+
1534+
# Same size as before, but with different keys below
1535+
# the iterators' current position.
1536+
d.clear()
1537+
d.update(dict.fromkeys(range(10)))
1538+
for i in range(3, 10):
1539+
del d[i]
1540+
1541+
for it in iterators:
1542+
with self.assertRaisesRegex(RuntimeError, 'keys changed'):
1543+
list(it)
1544+
15201545
def test_dict_copy_order(self):
15211546
# bpo-34320
15221547
od = collections.OrderedDict([('a', 1), ('b', 2)])
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Reverse iterators over a :class:`dict` and its views now raise
2+
:exc:`RuntimeError` if the dictionary's keys change during iteration, like
3+
forward iterators, instead of yielding entries for the new keys.

‎Objects/dictobject.c‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5717,6 +5717,12 @@ dictreviter_iter_lock_held(PyDictObject *d, PyObject *self)
57175717
value = entry_ptr->me_value;
57185718
}
57195719
}
5720+
// We found an element, but did not expect it
5721+
if (di->len == 0) {
5722+
PyErr_SetString(PyExc_RuntimeError,
5723+
"dictionary keys changed during iteration");
5724+
goto fail;
5725+
}
57205726
di->di_pos = i-1;
57215727
di->len--;
57225728

0 commit comments

Comments
 (0)