Rust 1.96's Range types: taking Copy back from Iterator
Range was never Copy because it implements Iterator itself. Rust 1.96 stabilised a new family of range types that implement only IntoIterator, which makes them Copy. Why the distinction matters, and when the familiar syntax changes.
Rust 1.96.0 (28 May 2026) stabilised something that looks tiny and will quietly seep into a lot of code: new range types.
The complaint behind it is one sentence: Range ought to be Copy, and it is not.
Why it is not
Because Range implements Iterator itself. A type that implements both Iterator and Copy is a footgun: Copy means the original is still usable after an assignment, so after for x in r the value r is still there — except you just consumed it. What does iterating again mean? Empty? From the start? The semantics collapse immediately.
So the standard library never gave Range a Copy, at the cost that it cannot conveniently be stored and reused. To keep “a slice of a slice” in a struct, you had to split it:
struct Span {
start: usize,
end: usize,
}
Silly, but there was no alternative.
RFC 3550’s answer: switch to IntoIterator
The newly stable types live under core::range:
core::range::Rangecore::range::RangeFromcore::range::RangeInclusive
The difference is exactly one thing: they implement IntoIterator, not Iterator. So Copy stops being a footgun — copying a range does not copy “how far the iteration got”, because that state is not in the type at all.
Which means the struct above becomes:
use core::range::Range;
#[derive(Clone, Copy)]
pub struct Span(Range<usize>);
impl Span {
pub fn of(self, s: &str) -> &str {
&s[self.0]
}
}
One more detail: the new RangeInclusive makes its fields public. The legacy one hid them to conceal the “already exhausted” iterator state. The new type has no such state, so there is nothing to hide.
The syntax has not switched
Writing 0..1 today still produces the legacy core::ops types. The stated plan is that “a future edition will update range syntax to core::range types”.
So:
- Migrating now means writing
core::range::Range { start, end }explicitly or converting - Existing
0..1code keeps its behaviour; nothing suddenly fails to compile - If you are writing new code and want to revise it less later, you can reach for the new types today
Coming later: core::range::RangeFull and core::range::RangeTo (they never implemented Iterator and were already Copy, so these are just re-exports), plus core::range::legacy::* as the new home for the current family.
Where I land
This is the kind of stabilisation whose diff is tiny and whose impact is slow. You will not save many lines today, but it repairs a long-standing design compromise:
“Can be iterated” and “can be copied” are contradictory at the type level, the standard library chose not to have both, and users were forced to split fields. The new types separate the concerns — iterable (IntoIterator) and copyable (Copy) never belonged welded together.
The signal worth watching is when the syntax switches. On that day the type of 0..1 changes, and any code relying on “Range is an Iterator” — passing r straight to something taking impl Iterator, for instance — starts failing. Knowing that ahead of time beats discovering it in a CI run.

Comments
…