19.6For loops, demystified

Last updated June 13, 2026

Back in lesson 7.5 we introduced the for loop and promised that one day you'd see what it really is. You now have every piece: the Iterator trait (19.3), next, and the iter/iter_mut/into_iter trio. This short lesson assembles them and pays the debt in full. A for loop is not a primitive of the language with special looping powers. It's a thin convenience that expands into an iterator and a while let.

What for actually does

Take the simplest possible loop:

let v = vec![10, 20, 30];
for x in v {
    println!("{x}");
}

The compiler rewrites this, roughly, into the following before compiling it:

let v = vec![10, 20, 30];
let mut iter = v.into_iter();
while let Some(x) = iter.next() {
    println!("{x}");
}

That's the whole trick. for x in thing does three things: call into_iter() on thing to get an iterator, then repeatedly call next(), binding each Some(x) to the loop variable and running the body, and stop the instant next() returns None. The while let you met in lesson 11.6 is doing the looping; the Option from next is doing the "are we done?" decision. There's no magic looping construct underneath, just next in a loop.

Run both versions and they print identically:

10
20
30

IntoIterator: the trait for calls

The method for calls is into_iter, and it comes from a trait named IntoIterator: "this type can be turned into an iterator." Anything that implements IntoIterator can sit on the right of for ... in. Vectors implement it, ranges implement it, arrays implement it, hash maps implement it. That's why all of those work in a for loop, they each say how to become an iterator, and for calls that method.

This also finally explains the & you've been writing in loop headers for nineteen chapters. References to collections implement IntoIterator too, and they do it by delegating to the borrowing iterators:

for x in &v        // calls (&v).into_iter()  ->  v.iter(),      x is &T
for x in &mut v    // calls (&mut v).into_iter() -> v.iter_mut(), x is &mut T
for x in v         // calls v.into_iter(),                        x is T (consumes v)

So for x in &v was never a special borrowing form of for. It was an ordinary for over &v, and &v's into_iter is iter. The three loop headers you've been choosing between are just three different things implementing IntoIterator, the value, the shared reference, and the mutable reference, each turning into the matching iterator. The choice you made at the top of every loop, v, &v, or &mut v, was choosing ownership versus borrow all along, dressed up as loop syntax.

Key insight

for is sugar for IntoIterator::into_iter followed by while let Some(x) = iter.next(). Everything you learned about iterators, laziness, the Item type, iter versus into_iter, applies to every for loop you've ever written, because every for loop is an iterator being driven. The loop was the iterator wearing a familiar coat the whole time.

Why this matters in practice

This isn't only trivia. Three practical consequences fall straight out of it.

First, anything that's an iterator works in a for loop with no .collect() in between, because for consumes iterators directly. for (i, x) in v.iter().enumerate() works because the adapter chain is already an iterator (every Iterator is trivially IntoIterator, it returns itself). You don't gather the adapter output into a vector first; for drives it as-is.

Second, the choice between a for loop and an adapter pipeline is a style choice, not a capability one, because they compile to the same machinery. A for loop with a mutable accumulator and the equivalent fold produce the same code; pick whichever reads more clearly for the task. The next lesson (19.7) gives that decision the attention it deserves.

Third, you can make your own types loopable. Implement IntoIterator for a type you wrote, and for item in my_value just works, calling your into_iter. That's how a custom collection becomes a first-class citizen, indistinguishable in use from Vec. It's beyond what we'll build here, but the door is now visibly open: the loop syntax was never reserved for built-in types.

Quiz time

Question #1

Rewrite for x in nums { sum += x; } as the while let loop the compiler turns it into.

Show solution
let mut iter = nums.into_iter();
while let Some(x) = iter.next() {
    sum += x;
}

for calls into_iter() on nums to get an iterator, then loops with while let Some(x) = iter.next(), running the body for each value and stopping when next() returns None.

Question #2

After this lesson, what is for x in &v actually doing, in terms of iterators?

Show solution

It's an ordinary for loop over the value &v. &v implements IntoIterator, and its into_iter() is v.iter(), which yields &T. So for x in &v is just for calling (&v).into_iter(), i.e. v.iter(). It was never a special "borrowing for"; the & chose the shared-reference iterator, and for x in &mut v / for x in v similarly pick iter_mut / into_iter.

Question #3

Why can you write for (i, c) in text.chars().enumerate() without calling .collect() first?

Show solution

Because text.chars().enumerate() is already an iterator, and every iterator implements IntoIterator (its into_iter returns itself). for calls into_iter on whatever follows in, so it drives the adapter chain directly. Collecting into a Vec first would just build a throwaway collection for for to take apart again.

You've now seen that loops and iterator pipelines are two spellings of the same thing. The final lesson before the quiz (19.7) weighs them against each other: performance (with a real benchmark), readability, and when each is the right call.