The Iterator and IntoIterator Traits
Understand the core traits that power iteration in Rust, what Iterator and IntoIterator do, how they work together, and how to implement them.
Iteration is so fundamental that Rust doesn't have a traditional C‑style for loop at all. Instead, every loop over a sequence relies on traits. Two traits in particular: Iterator, which knows how to yield values one by one, and IntoIterator, which turns a collection (or anything else) into an iterator. Once you understand these two, you understand how every for loop, every .map(), every .collect() call fits together.
The Iterator Trait
Iterator is the trait for types that can produce a sequence of values. Its core is deceptively small:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
There is one required method: next. Every time you call next, the iterator either returns Some(value) or None to signal it has run out. The associated type Item tells you what kind of value that is.
All the other iterator methods you know — map, filter, take, collect, fold, and dozens more — are provided methods. They all build on next, so you get them for free once you implement that single method.
Implementing Your Own Iterator
Think of an iterator as a small state machine. The struct holds whatever information it needs to know where it is in the sequence, and next moves the state forward.
Here is an iterator that counts from 1 to 5:
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Self {
Counter { count: 0 }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
self.count += 1;
if self.count <= 5 {
Some(self.count)
} else {
None
}
}
}
Now we can drive it manually:
let mut counter = Counter::new();
assert_eq!(counter.next(), Some(1));
assert_eq!(counter.next(), Some(2));
// ... eventually returns None
The state (count) is updated inside next. Once it passes 5, the iterator is exhausted and every subsequent call returns None. That is the entire contract.
Everything is working:
If calling next() on your iterator produces the values you expect in order, and eventually starts returning None, you have implemented Iterator correctly. All the provided methods will now work automatically.
Why This Design Exists
In many languages iteration involves an index variable, a length check, and manual increments. That pattern is error‑prone: off‑by‑one bugs, accidental mutation of the counter, and mixing iteration logic with business logic are all common. Rust makes the iterator the single source of truth about what the sequence is and how to advance through it. The for loop and all adapters know nothing about the data structure; they only ask the iterator what comes next. That separation makes iteration composable and safe.
A Beginner’s Mental Model
If you have never used iterators before, picture a roll of paper towels. Each time you pull, you get one sheet. The roll keeps track of how much is left. next is that pull. Item is the type of thing you get (in this case, a number). When the roll is empty, you get None.
The provided methods like map wrap your original iterator in a new one that modifies each sheet as it comes out. filter wraps it in one that skips sheets that don’t match a condition. None of this happens until you start pulling; that is laziness.
IntoIterator and for Loops
While Iterator knows how to produce values, it does not know how to start the process from a collection. That job belongs to IntoIterator.
pub trait IntoIterator {
type Item;
type IntoIter: Iterator<Item = Self::Item>;
fn into_iter(self) -> Self::IntoIter;
}
A type that implements IntoIterator can be turned into an iterator. The associated type IntoIter is the concrete iterator type that gets created. Notice that IntoIter must implement Iterator with the same Item. So IntoIterator is a factory: give me self, and I will give you an iterator that yields elements of type Item.
How for Loops Actually Work
When you write:
let numbers = vec![1, 2, 3];
for n in numbers {
println!("{}", n);
}
The compiler desugars it to something like this:
let numbers = vec![1, 2, 3];
{
let mut iter = IntoIterator::into_iter(numbers);
loop {
match iter.next() {
Some(n) => {
println!("{}", n);
}
None => break,
}
}
}
The for loop calls into_iter() on the value you give it, then repeatedly calls next() until it sees None. That means for works with any type that implements IntoIterator — not just iterators themselves.
The Three Forms of Iteration
A collection can be turned into an iterator in three ways, depending on how you access the data:
- By value:
for item in collection— moves the collection and yields owned items. The collection is consumed. - By shared reference:
for item in &collection— borrows immutably, yields&Item. - By mutable reference:
for item in &mut collection— borrows mutably, yields&mut Item.
These are not three methods on IntoIterator; they are three separate implementations of the trait. The compiler picks the right one based on the type of the expression after in.
To see this in practice, we’ll implement a tiny collection Solution that holds a Vec<Project> and make it iterable in all three ways.
pub struct Project {
pub name: String,
}
pub struct Solution {
projects: Vec<Project>,
}
// By value (consumes Solution)
impl IntoIterator for Solution {
type Item = Project;
type IntoIter = std::vec::IntoIter<Project>;
fn into_iter(self) -> Self::IntoIter {
self.projects.into_iter()
}
}
// By shared reference
impl<'a> IntoIterator for &'a Solution {
type Item = &'a Project;
type IntoIter = std::slice::Iter<'a, Project>;
fn into_iter(self) -> Self::IntoIter {
self.projects.iter()
}
}
// By mutable reference
impl<'a> IntoIterator for &'a mut Solution {
type Item = &'a mut Project;
type IntoIter = std::slice::IterMut<'a, Project>;
fn into_iter(self) -> Self::IntoIter {
self.projects.iter_mut()
}
}
Now all three forms work:
let mut sln = Solution {
projects: vec![
Project { name: "Apollo".into() },
Project { name: "Artemis".into() },
],
};
// By move – sln is consumed after this
for proj in sln {
println!("Project: {}", proj.name);
}
// sln is moved, so the following would not compile unless we recreate it.
// We'll recreate for the other examples.
let mut sln2 = Solution {
projects: vec![
Project { name: "Apollo".into() },
Project { name: "Artemis".into() },
],
};
// By immutable reference
for proj in &sln2 {
println!("Project: {}", proj.name);
}
// By mutable reference
for proj in &mut sln2 {
proj.name = proj.name.to_uppercase();
}
The lifetime annotations tie the borrowed iterator to the borrow of Solution. The reference to the collection must live at least as long as the iterator.
Beware of consuming loops:
A for loop over an owned value like sln will move the collection. If you try to use sln after the loop, the compiler will reject the code because sln no longer owns anything. Always check whether you intended a move or a borrow. Use &collection or .iter() when you need to keep the original collection alive.
All Iterators Implement IntoIterator
There is a blanket implementation in the standard library:
impl<I: Iterator> IntoIterator for I {
type Item = I::Item;
type IntoIter = I;
fn into_iter(self) -> I { self }
}
Every iterator is also an iterable. If you call into_iter() on an iterator, you simply get the same iterator back. This has a direct practical consequence: you can use any iterator directly in a for loop.
let iter = (1..4).map(|x| x * 2);
for val in iter {
println!("{}", val); // prints 2, 4, 6
}
Here iter is already an iterator (a Map). The for loop calls IntoIterator::into_iter(iter), which just returns iter. No conversion needed.
This blanket impl also means you cannot implement IntoIterator for a type that already implements Iterator. You would end up with two conflicting implementations. If you write a custom iterator, you automatically get IntoIterator for it — you never need to implement IntoIterator for that type again, except for reference types (like &MyIter), which are different types.
Don't implement IntoIterator for the same type that already implements Iterator:
If you have struct MyIter; and impl Iterator for MyIter { ... }, you already get impl IntoIterator for MyIter via the blanket impl. Adding another impl IntoIterator for MyIter will cause a coherence conflict. The compiler error will mention conflicting implementations. If you need to provide additional ways to convert (e.g., from a reference), implement IntoIterator for &MyIter, which is a different type.
How the Two Traits Work Together
When you write a pipeline like:
let sum: i32 = vec![1, 2, 3]
.iter()
.map(|x| x * 2)
.sum();
several things happen:
.iter()creates a slice iterator over&i32..map(...)consumes that iterator and returns aMapstruct, which implementsIterator..sum()is a consumer method onIteratorthat repeatedly callsnextand adds up the values.
At no point do you call into_iter explicitly, because all the adapters return types that are already iterators, and sum works directly on any iterator. The for loop would work on the Map as well, thanks to the blanket IntoIterator impl.
If, however, you started with a collection and wrote for x in vec![1,2,3], the compiler calls IntoIterator::into_iter on the Vec<i32> to get a std::vec::IntoIter, then drives it with next.
Choosing Between iter(), iter_mut(), and into_iter()
Many collections expose these three methods explicitly, even though the for loop can pick them up through IntoIterator:
| Method | Returns | Borrowing | Use when |
|---|---|---|---|
.iter() | Iter (yields &T) | Shared | You only need read access to elements, collection lives on |
.iter_mut() | IterMut (yields &mut T) | Mutable | You need to modify elements in place |
.into_iter() | IntoIter (yields T) | Owned / move | You want to consume the collection and take ownership of items |
When you call these methods in a function chain, you are explicitly choosing the iterator type. The for loop does this automatically depending on whether you give it a value, a reference, or a mutable reference.
The IntoIterator bound in generic code:
When you write a function that only needs to iterate over its argument, accept impl IntoIterator<Item = T> rather than &[T] or Vec<T>. This makes the function work with any iterator, array, vector, or other iterable. Example:
fn print_all(items: impl IntoIterator<Item = i32>) {
for item in items {
println!("{}", item);
}
}
This single function accepts a Vec<i32>, a &[i32], a Range<i32>, or any custom iterable.
Common Mistakes When Implementing IntoIterator
The trait signature can be confusing, especially the associated type IntoIter. A frequent error is trying to use Iterator as a type instead of a concrete struct:
// ERROR: the trait `Iterator` is not sized
impl IntoIterator for Pixel {
type Item = i32;
type IntoIter = Iterator<Item = Self::Item>;
// ...
}
Iterator is a trait, not a concrete type. You must name a specific type that implements Iterator, such as std::vec::IntoIter<Pixel> or a custom struct you define. The error message about Sized often points to this mistake.
If your iterator type is complex, you can use impl Iterator in the return type of a function, but you cannot use it as an associated type in a trait implementation (unless you are on nightly with type_alias_impl_trait). The stable way is to define a named struct that implements Iterator and use that struct name.
Another subtlety: when implementing IntoIterator for a reference, the lifetime of the returned iterator must be tied to the lifetime of the borrow. Omitting the lifetime or getting it wrong leads to borrow‑checker errors. The template from earlier (impl<'a> IntoIterator for &'a Solution) shows the correct pattern.
Summary
The Iterator and IntoIterator traits form the foundation of all iteration in Rust. Iterator handles the mechanics of producing values via next. IntoIterator handles the conversion from a container (or anything else) into an iterator. Together they power for loops, adapter chains, and collection methods.
The key insight is that these two traits separate what you iterate over from how you produce the sequence. Any type that implements IntoIterator can be looped over. Any type that implements Iterator can be transformed, filtered, and collected.
If you are building a collection type, implement IntoIterator for it (with all three reference variants if appropriate). If you are building a custom sequence, implement Iterator. In most Rust code you will consume iterators far more often than you implement them, but understanding what’s underneath makes the whole system feel predictable rather than magical.