Summary
Pickling (or otherwise __reduce__-round-tripping) a partially consumed dict iterator or dict-view iterator (dict_keyiterator / dict_valueiterator / dict_itemiterator) discards the current position and restarts from the beginning. set / list / tuple / str iterators are unaffected.
Reproduction
import pickle
d = {'a': 1, 'b': 2, 'c': 3}
for kind, factory in [
('dict', lambda: iter(d)),
('keys', lambda: iter(d.keys())),
('values', lambda: iter(d.values())),
('items', lambda: iter(d.items())),
]:
it = factory(); next(it) # consume one element
print(kind, list(pickle.loads(pickle.dumps(it))))
| iterator |
RustPython |
CPython 3.14 |
iter(d) |
['a', 'b', 'c'] ❌ |
['b', 'c'] |
iter(d.keys()) |
['a', 'b', 'c'] ❌ |
['b', 'c'] |
iter(d.values()) |
[1, 2, 3] ❌ |
[2, 3] |
iter(d.items()) |
[('a', 1), ('b', 2), ('c', 3)] ❌ |
[('b', 2), ('c', 3)] |
iter({1,2,3}), iter([1,2,3]), iter((1,2,3)), iter('abc') |
correct ✅ |
correct |
Looking directly at __reduce__ after consuming one key:
d = {'a': 1, 'b': 2, 'c': 3}
ki = iter(d.keys()); next(ki)
print(ki.__reduce__())
# RustPython: (<built-in function iter>, (['a', 'b', 'c'],)) <- all keys
# CPython: (<built-in function iter>, (['b', 'c'],)) <- remaining keys
A freshly created (unconsumed) dict iterator reduces identically on both, so the divergence appears only after at least one next().
Root cause
crates/vm/src/builtins/dict.rs:1060 — the shared dict-iterator __reduce__ materializes every entry via dict.into_iter() and ignores internal.position, returning a 2-tuple (iter, (full_list,)) with no state component, so there is nothing to restore the position from:
fn __reduce__(&self, vm: &VirtualMachine) -> PyTupleRef {
let iter = builtins_iter(vm);
let internal = self.internal.lock();
let entries = match &internal.status {
IterStatus::Active(dict) => dict
.into_iter() // <- all entries, position ignored
.map(|(key, value)| ($result_fn)(vm, key, value))
.collect::<Vec<_>>(),
IterStatus::Exhausted => vec![],
};
vm.new_tuple((iter, (vm.ctx.new_list(entries),)))
}
dictiter_reduce in CPython builds the list from the remaining items (current position onward). The fix is to skip entries up to internal.position (mirroring next_entry) before collecting.
The reverse-iterator variant at crates/vm/src/builtins/dict.rs:1129 has the same shape and additionally carries a // TODO: entries must be reversed too note, so it is also affected.
Reference
Verified against CPython 3.14.5. list/tuple/str/set iterators already behave correctly, so the fix is localized to the dict iterator macro.
Investigated and drafted by Claude; reviewed before filing.
Related representational divergence in range-iterator __reduce__: #8377.
Summary
Pickling (or otherwise
__reduce__-round-tripping) a partially consumeddictiterator or dict-view iterator (dict_keyiterator/dict_valueiterator/dict_itemiterator) discards the current position and restarts from the beginning.set/list/tuple/striterators are unaffected.Reproduction
iter(d)['a', 'b', 'c']❌['b', 'c']iter(d.keys())['a', 'b', 'c']❌['b', 'c']iter(d.values())[1, 2, 3]❌[2, 3]iter(d.items())[('a', 1), ('b', 2), ('c', 3)]❌[('b', 2), ('c', 3)]iter({1,2,3}),iter([1,2,3]),iter((1,2,3)),iter('abc')Looking directly at
__reduce__after consuming one key:A freshly created (unconsumed) dict iterator reduces identically on both, so the divergence appears only after at least one
next().Root cause
crates/vm/src/builtins/dict.rs:1060— the shared dict-iterator__reduce__materializes every entry viadict.into_iter()and ignoresinternal.position, returning a 2-tuple(iter, (full_list,))with no state component, so there is nothing to restore the position from:dictiter_reducein CPython builds the list from the remaining items (current position onward). The fix is to skip entries up tointernal.position(mirroringnext_entry) before collecting.The reverse-iterator variant at
crates/vm/src/builtins/dict.rs:1129has the same shape and additionally carries a// TODO: entries must be reversed toonote, so it is also affected.Reference
Verified against CPython 3.14.5.
list/tuple/str/setiterators already behave correctly, so the fix is localized to the dict iterator macro.Investigated and drafted by Claude; reviewed before filing.