Skip to content

range_iterator.__reduce__ uses original range + index state instead of rebased range + None #8377

Description

@youknowone

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.

Metadata

Metadata

Assignees

Labels

C-compatA discrepancy between RustPython and CPython

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions