Skip to content

dict / dict-view iterators lose position when pickled (__reduce__ ignores consumed items) #8376

Description

@youknowone

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.

Metadata

Metadata

Assignees

Labels

C-compatA discrepancy between RustPython and CPythonz-ca-2026Tag to track Contribution Academy 2026

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions