Summary
range_iterator.__reduce__ (and longrange_iterator.__reduce__) return the original range plus the current index as pickle state, whereas CPython returns the remaining range rebased to the current position with state None. The round-trip result is correct in both, so this is a representational / cross-compatibility divergence rather than a data-loss bug.
Reproduction
it = iter(range(10)); next(it); next(it); next(it)
print(it.__reduce__())
# RustPython: (<built-in function iter>, (range(0, 10),), 3)
# CPython: (<built-in function iter>, (range(3, 10),), None)
print(iter(range(3)).__reduce__())
# RustPython: (<built-in function iter>, (range(0, 3),), 0)
# CPython: (<built-in function iter>, (range(0, 3),), None)
print(reversed(range(3)).__reduce__())
# RustPython: (<built-in function iter>, (range(2, -1, -1),), 0)
# CPython: (<built-in function iter>, (range(2, -1, -1),), None)
list(pickle.loads(pickle.dumps(it))) yields the same values on both implementations, because RustPython restores the index through __setstate__. The difference is only in the __reduce__ representation:
- CPython: 3-tuple whose state is always
None, and the range argument is advanced to the remaining span.
- RustPython: 3-tuple whose state is the raw
index, and the range argument is the unmodified original.
Code that inspects __reduce__ directly, or that relies on pickles being interchangeable with CPython, sees the difference.
Root cause
crates/vm/src/builtins/range.rs:711 range_iter_reduce embeds the full original range and passes index as the third tuple element:
fn range_iter_reduce(start, length, step, index, vm) -> PyTupleRef {
let iter = builtins_iter(vm);
let stop = start.clone() + length * step.clone();
let range = PyRange { start: ..start.., stop: ..stop.., step: ..step.. };
vm.new_tuple((iter, (range,), index)) // original range + index state
}
Called from PyRangeIterator::__reduce__ (range.rs:676) and PyLongRangeIterator::__reduce__ (range.rs:614). To match CPython, rebase the range start by index * step and emit None for the state (the existing __setstate__ can remain for backward compatibility with pickles that carry an integer state).
Reference
Verified against CPython 3.14.5. Related: dict-iterator __reduce__ has a separate, more severe correctness bug (loses position entirely) — #8376.
Investigated and drafted by Claude; reviewed before filing.
Summary
range_iterator.__reduce__(andlongrange_iterator.__reduce__) return the original range plus the current index as pickle state, whereas CPython returns the remaining range rebased to the current position with stateNone. The round-trip result is correct in both, so this is a representational / cross-compatibility divergence rather than a data-loss bug.Reproduction
list(pickle.loads(pickle.dumps(it)))yields the same values on both implementations, because RustPython restores the index through__setstate__. The difference is only in the__reduce__representation:None, and the range argument is advanced to the remaining span.index, and the range argument is the unmodified original.Code that inspects
__reduce__directly, or that relies on pickles being interchangeable with CPython, sees the difference.Root cause
crates/vm/src/builtins/range.rs:711range_iter_reduceembeds the full original range and passesindexas the third tuple element:Called from
PyRangeIterator::__reduce__(range.rs:676) andPyLongRangeIterator::__reduce__(range.rs:614). To match CPython, rebase the range start byindex * stepand emitNonefor the state (the existing__setstate__can remain for backward compatibility with pickles that carry an integer state).Reference
Verified against CPython 3.14.5. Related: dict-iterator
__reduce__has a separate, more severe correctness bug (loses position entirely) — #8376.Investigated and drafted by Claude; reviewed before filing.