Rust 1.96 turns ranges into safer copies
Rust 1.96.0 adds new Range* types that are Copy-friendly and fix a nasty Iterator footgun.

Rust 1.96.0 adds Copy-friendly Range types that remove a common iterator footgun.
I've been using Rust long enough that I stopped trusting anything that looks “obviously simple.” Ranges were one of those things. I’d reach for 0..n, wire it into a helper, stash it in a struct, and then hit the same annoying wall: the type I thought was just a plain span over numbers was also acting like an iterator with state. That’s where Rust starts doing its usual thing, which is usually great until it isn’t. The old range types were convenient, but they also carried enough weirdness that I kept tripping over ownership rules in places that should have been boring. And boring is what I want from a range.
What really bothered me was the mismatch between how I used ranges and how the library treated them. I wanted to copy them around, store them, pass them through APIs, and not think about whether I had consumed an iterator by accident. Instead, I had to remember that some range-ish types were iterator-ish in ways that made them awkward to reuse. That’s the sort of paper cut that doesn’t look dramatic in a changelog, but it infects real code. Rust 1.96.0 finally addresses that mess with new range types that behave the way most of us expected in the first place.
The trigger for this breakdown was Paul Krill’s InfoWorld piece, "Rust introduces new Range types", which covers the stabilization of the new core::range types in Rust 1.96.0. The key detail is not the version number. It’s the cleanup: Rust is separating the “range as a value” idea from the “range as iterator state” idea, and that’s long overdue.
Rust finally stops pretending ranges are just iterators
Get the latest AI news in your inbox
Weekly picks of model releases, tools, and deep dives — no spam, unsubscribe anytime.
No spam. Unsubscribe at any time.
"It is a footgun to implement both Iterator and Copy on the same type"
What this actually means is that the old range types were too easy to misuse. A type that can be copied around freely should not also behave like a stateful iterator unless you really know what you’re doing. Rust’s standard library team is basically admitting that the old design mixed two different responsibilities.

The article says the new replacement range types implement IntoIterator rather than Iterator, which is the important shift. That sounds tiny, but it changes how the type behaves in everyday code. Instead of the range itself being the iterator, the range becomes a value that can produce an iterator when needed. That means the range can also be Copy, which is exactly what developers expected when they first saw something like 0..10.
I’ve run into this sort of confusion in APIs that try to be clever. They save a line of code up front and cost you an hour later when ownership gets weird. Rust usually avoids that trap, but ranges slipped through with a design that made sense historically and got awkward in practice. This update is Rust cleaning up its own mess, which I appreciate more than shiny new syntax.
How to apply it: if you’re designing your own types, separate “this is a reusable description of work” from “this is the thing currently doing the work.” If your type is meant to be copied, don’t make the type itself carry hidden iteration state unless you absolutely need it.
- Use value types for reusable descriptions.
- Use iterators for consumed state.
- When in doubt, make the conversion explicit.
core::range is the boring API I wanted all along
The new stable types are core::range::Range, core::range::RangeFrom, core::range::RangeInclusive, and associated iterators. The article also says core::range::RangeFull and core::range::RangeTo will show up later as re-exports from core::ops. That’s a lot of names, but the idea is simple: Rust is moving the “good” version of ranges into a namespace that reflects their real purpose.
What this actually means is that the standard library now has a proper home for range values that are meant to be copied, stored, and passed around. The old range syntax like 0..1 still produces legacy types for now, so nobody’s code explodes today. But the direction is obvious: the language wants the syntax to map to the new range family in a future edition.
I like this because it gives library authors a cleaner target. If I’m writing code that needs to hold onto a range, I want a type that behaves like data, not a half-consumed iterator with personality problems. The new namespace also makes it easier to explain in docs and code review. “Use core::range for reusable range values” is a sentence I can live with.
How to apply it: when you introduce a new API, name the stable concept after the thing users actually mean, not the implementation detail you happened to start with. If your public type is both data and behavior, you’re probably going to make somebody miserable later.
- Prefer namespaces that reflect user intent.
- Keep legacy behavior isolated during transitions.
- Document the migration path before changing syntax.
The real win is Copy-friendly slice accessors
InfoWorld notes that with these stabilizations, it’s now possible to store slice accessors in Copy types without splitting start and end. That’s the kind of sentence that looks dry until you’ve actually had to do the workaround yourself.

What this actually means is that code can carry around a range-like value without manually decomposing it into two fields just to satisfy the borrow checker. That’s a huge quality-of-life improvement for APIs that need to keep a slice window around. Instead of storing start and end separately, you can store the range itself and let the type system stop being annoying for once.
I’ve done the split-field workaround. It works, but it’s ugly and it leaks implementation detail into your struct layout. You end up with code that says “I know this is a range, but I’m pretending it isn’t because the type system made me.” That’s not a design. That’s a surrender.
How to apply it: if you have a range-like concept in your own code, ask whether users need the endpoints independently or whether they just need the interval as a unit. If it’s the latter, don’t force them to split it. Give them a single value they can copy.
- Store ranges as ranges when possible.
- Split endpoints only when each endpoint has separate meaning.
- Prefer API shapes that match the mental model users already have.
Public fields on RangeInclusive are a tell, not a flourish
The article says the new RangeInclusive type makes its fields public, unlike the legacy version that hid the exhausted iterator state. That detail matters more than it first appears. The old type had to protect internal iterator bookkeeping. The new one doesn’t, because it’s not pretending to be an iterator anymore.
What this actually means is that the type is simpler and more honest. If a type is just representing a range, then its fields can be exposed without dragging along iterator exhaustion semantics. Rust is removing a layer of defensive weirdness that only existed because the old abstraction was doing double duty.
I’ve always thought public fields get a bad reputation when the real issue is whether the abstraction is clean. Public fields on a messy type are a problem. Public fields on a straight-up data carrier are fine. This change says the new range types are data carriers first, iterator machinery second.
How to apply it: when you design a type, don’t hide fields just because “encapsulation.” Hide them when there’s real invariants to protect. If the type is basically a tuple with a name, exposing fields can make the API easier to use and easier to reason about.
For anyone maintaining libraries, this also means the migration story matters. If you’ve built helpers around legacy RangeInclusive behavior, you should expect the new types to be less forgiving about iterator-style assumptions. That’s good. Weird hidden state is how bugs breed.
Rust is still keeping legacy ranges around, and that’s the right call
The article says range syntax like 0..1 still produces the legacy types for now, and the team plans to move that syntax over in an upcoming edition. It also says a future version will introduce core::range::legacy::* as the new home for the current ranges. That’s a classic Rust migration move: keep the old thing working, name it honestly, and move forward without breaking everyone on Tuesday.
What this actually means is that there’s a bridge period where both models exist. That’s not glamorous, but it’s how you avoid forcing the whole ecosystem to rewrite code just because the standard library got smarter. I respect that. Rust can be strict without being reckless.
I ran into a similar migration pattern in a large codebase where we had to replace one internal date type with another. The worst mistake would have been a flag day. The better move was exactly what Rust is doing here: introduce the new API, keep the old one available, and make the old naming say “legacy” so nobody pretends it’s the long-term answer.
How to apply it: if you’re shipping a breaking cleanup, give users a path. Keep the old API available long enough to migrate, but don’t let the old naming pretend it’s the future. Honest names reduce confusion.
The new macros are a nice side dish, not the main course
Rust 1.96.0 also adds assert_matches! and debug_assert_matches!, which check whether a value matches a pattern and panic with a Debug representation if it doesn’t. That’s useful, but I’d treat it as a convenience feature next to the range work.
What this actually means is that Rust is making pattern-based assertions easier to write and easier to debug. If you’ve ever written a test that needed to confirm a value matched a specific enum shape, this saves a little ceremony and gives better failure output than a hand-rolled check.
I like additions like this because they remove friction without changing how I think about the program. The range update changes how I model data. These macros just make my tests less annoying. Useful, yes. The headline? Not for me.
How to apply it: use pattern assertions in tests when the shape matters more than the exact value. Keep them out of production logic unless they genuinely simplify error handling or invariant checks.
WebAssembly got stricter, and that’s a feature
The article also notes that WebAssembly targets no longer pass --allow-undefined to the linker. Undefined symbols now trigger a linker error instead of quietly becoming WebAssembly imports from the env module. That’s Rust choosing earlier failure over mystery behavior.
What this actually means is that the toolchain will catch missing symbols before you ship a broken module or accidentally depend on a naming mistake. I’ve seen enough build systems to know that “it linked somehow” is not a compliment. It’s usually a warning sign.
This part matters because it shows the same philosophy as the range change: remove hidden behavior that surprises people later. Whether the surprise is iterator state or undefined symbols, the fix is the same. Make the failure obvious and make the model simpler.
How to apply it: if you maintain build tooling or cross-target code, prefer strict linking and early validation. Hidden imports and implicit fallbacks are the kind of thing that make debugging miserable three weeks later.
The template you can copy
## What changed in Rust 1.96.0
Rust 1.96.0 introduces new `core::range` types that separate reusable range values from iterator state.
The big win is that these range types are `Copy`-friendly and avoid the old `Iterator` + `Copy` footgun.
### Why this matters
- Store ranges as values instead of splitting `start` and `end`
- Use `IntoIterator` when a range needs to produce iteration behavior
- Keep iterator state out of types that should be freely copied
### Practical migration notes
- Existing syntax like `0..1` still uses legacy range types for now
- Plan for future editions to map range syntax to `core::range`
- Treat `core::range::legacy::*` as compatibility territory, not the long-term API
### Design rule I’m using
If a type is meant to be copied and reused, it should describe work, not perform work.
If it needs state, make that state explicit.
### Example pattern
rust
use core::range::Range;
#[derive(Copy, Clone)]
struct Window {
bounds: Range,
}
impl Window {
fn new(bounds: Range) -> Self {
Self { bounds }
}
}
### Test helper idea
rust
assert_matches!(value, Some(MyEnum::Ready { .. }));
### My rule of thumb
- Data-like range? Make it copyable.
- Iterator-like state? Make it consumed.
- Legacy compatibility? Keep it named honestly.
That template is mine, but the underlying idea comes from the Rust team’s range cleanup and Paul Krill’s InfoWorld summary of Rust 1.96.0. I’ve adapted it into something you can paste into docs, an internal memo, or a migration note without rewriting the whole explanation.
Source: InfoWorld article. For the upstream Rust side, start with the RFC 3550 discussion and the Rust blog for release details. The template and commentary here are my synthesis, not a verbatim rewrite.
// Related Articles
- [TOOLS]
RustRover 2026.1.4 is the right default IDE for Rust teams
- [TOOLS]
Claude Design setup for synced prototypes
- [TOOLS]
AI Data Operations vs MLOps: what each owns
- [TOOLS]
OpenTag turns Slack threads into actions
- [TOOLS]
GPU VRAM Needed for LLM Fine-Tuning in 2026
- [TOOLS]
Claude Sonnet 5 上手部署与评估