Skip to content

Commit 042a7c1

Browse files
jabclaude
andauthored
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 <noreply@anthropic.com>
1 parent 302ce2d commit 042a7c1

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
@@ -1424,6 +1424,31 @@ def test_reversed_dict_after_clear_and_restore(self):
14241424
for it in iterators:
14251425
self.assertEqual(list(it), [])
14261426

1427+
def test_reversed_dict_keys_changed_during_iteration(self):
1428+
d = dict.fromkeys(range(10))
1429+
for i in range(7):
1430+
del d[i]
1431+
1432+
iterators = (
1433+
reversed(d),
1434+
reversed(d.keys()),
1435+
reversed(d.values()),
1436+
reversed(d.items()),
1437+
)
1438+
for it in iterators:
1439+
next(it)
1440+
1441+
# Same size as before, but with different keys below
1442+
# the iterators' current position.
1443+
d.clear()
1444+
d.update(dict.fromkeys(range(10)))
1445+
for i in range(3, 10):
1446+
del d[i]
1447+
1448+
for it in iterators:
1449+
with self.assertRaisesRegex(RuntimeError, 'keys changed'):
1450+
list(it)
1451+
14271452
def test_dict_copy_order(self):
14281453
# bpo-34320
14291454
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
@@ -6303,6 +6303,12 @@ dictreviter_iter_lock_held(PyDictObject *d, PyObject *self)
63036303
value = entry_ptr->me_value;
63046304
}
63056305
}
6306+
// We found an element, but did not expect it
6307+
if (di->len == 0) {
6308+
PyErr_SetString(PyExc_RuntimeError,
6309+
"dictionary keys changed during iteration");
6310+
goto fail;
6311+
}
63066312
di->di_pos = i-1;
63076313
di->len--;
63086314

0 commit comments

Comments
 (0)