7.5for loops and ranges

Last updated June 12, 2026

Chapter 4 ended with a debt. The falling-ball program (4.x) closed on six lines you were instructed to find irritating:

report(start, 0.0);
report(start, 1.0);
report(start, 2.0);
report(start, 3.0);
report(start, 4.0);
report(start, 5.0);

The same call, six times, with a number counting up. You could write this with a while loop now (counter, condition, update, three pieces of bookkeeping to keep aligned). Counting over a known stretch of numbers is so common that it gets its own loop, and the bookkeeping is the loop's job:

for seconds in 0..=5 {
    report(start, seconds as f64);
}

Six lines become two, and there's nothing to keep aligned: no counter to initialize, no condition to get backwards, no += 1 to forget. (The as f64 is lesson 4.10 earning a callback: the range produces integers, and report wants an f64.)

Ranges

A for loop walks through a sequence of values, running its block once per value. The sequences you'll use most are ranges:

fn main() {
    for i in 0..5 {
        print!("{i} ");
    }
    println!();
    for i in 0..=5 {
        print!("{i} ");
    }
    println!();
}
0 1 2 3 4 
0 1 2 3 4 5 

0..5 is a half-open range: it starts at 0 and stops before 5. 0..=5 is an inclusive range: the = pulls the endpoint in. Both kinds appear constantly in Rust, and the half-open one is the default idiom for "do this N times" (0..n runs exactly n times, a fact worth saying out loud once: not n+1, not n-1).

Why two spellings? Because they correspond to the two things you actually mean. "The first five numbers" is 0..5; "one through five" is 1..=5. In a while loop, that distinction lives in your choice of < versus <=, ten characters apart and easy to fumble; the resulting mistakes are called off-by-one errors, and they're the most common loop bug in existence. The range syntax compresses the decision into "which dots did you type," right next to the numbers they govern. You can still get it wrong, but you can no longer get it wrong far away from where it matters.

The loop variable (i above) is a fresh, immutable binding each iteration; the loop creates it, hands it the next value, runs your block, and throws it away. No mut, no leftover counter after the loop ends. The tightest-scope advice from last lesson isn't advice here; it's just how for works.

Walking backwards, and skipping

Two range gadgets you'll want immediately. .rev() reverses a range, and .step_by(n) takes every n-th value:

fn main() {
    for i in (1..=3).rev() {
        println!("{i}...");
    }
    println!("liftoff!");
    for i in (0..=20).step_by(5) {
        print!("{i} ");
    }
    println!();
}
3...
2...
1...
liftoff!
0 5 10 15 20 

Note last lesson's unsigned-countdown trap doesn't exist here: nothing is ever decremented, so there's nothing to underflow. The range counts up and .rev() serves it backwards.

Trust us for now

The parentheses and the dot hint at the truth: ranges are values with methods, and for actually works with a whole family of types called iterators, of which ranges are just the friendliest member. Chapter 19 demystifies the machinery (and explains the small print, like why .rev() needs a range with a reachable end). Until then, (a..=b).rev() and (a..b).step_by(n) as recipes will not steer you wrong.

If you're arriving from C-family languages, you may be missing the three-part for (init; condition; update) about now, with its comma operators and its for(;;) idiom and its freedom to mutate the counter mid-flight. Rust doesn't have it. The honest accounting: the three-part form is a while loop in a trench coat, and Rust kept the while loop. What for does here is the other job, the one C's for only gestures at: visit every element of a sequence, no bookkeeping exposed. When chapter 18 introduces collections, the same loop walks arrays and lists without any index in sight, and that's where this design earns its keep.

Best practice

Reach for for when the values to visit are known up front (a range, eventually a collection). Reach for while when iteration depends on a condition you re-evaluate. Reach for loop when only something inside the loop can decide. Most loops you write should be for loops.

Quiz time

Question #1

What does this print?

fn main() {
    for n in (0..10).step_by(3) {
        print!("{n} ");
    }
    println!();
    for n in (1..=3).rev() {
        print!("{n} ");
    }
    println!();
}
Show solution
0 3 6 9 
3 2 1 

The first range starts at 0 and steps by 3 while staying under 10 (half-open). The second is 1, 2, 3 served in reverse.

Question #2

Write a program that prints every even number from 0 to 20, inclusive, two ways: once using step_by, once using a full range and an if (lesson 6.2's % is the even test).

Show solution
fn main() {
    for n in (0..=20).step_by(2) {
        print!("{n} ");
    }
    println!();
    for n in 0..=20 {
        if n % 2 == 0 {
            print!("{n} ");
        }
    }
    println!();
}
0 2 4 6 8 10 12 14 16 18 20 
0 2 4 6 8 10 12 14 16 18 20 

Both work; the first says "visit every second number," the second says "visit every number, act on the even ones." Prefer whichever reads as what you mean. Note both ranges are inclusive, since the problem said "to 20, inclusive."

Question #3

Write a function sum_to(value: u32) -> u32 that returns the sum of every number from 1 to value inclusive, using a for loop. sum_to(5) should return 15 (1+2+3+4+5).

Show solution
fn sum_to(value: u32) -> u32 {
    let mut sum = 0;
    for i in 1..=value {
        sum += i;
    }
    sum
}

fn main() {
    println!("{}", sum_to(5));
    println!("{}", sum_to(100));
}
15
5050

The accumulator sum needs mut (it changes), but the loop variable i doesn't (the loop re-creates it). An empty case falls out free: sum_to(0) walks the range 1..=0, which contains nothing, and returns 0.

Question #4

The classic. Print the numbers 1 through 15, except: for multiples of 3 print fizz instead, for multiples of 5 print buzz, and for multiples of both print fizzbuzz.

Show solution
fn main() {
    for n in 1..=15 {
        if n % 15 == 0 {
            println!("fizzbuzz");
        } else if n % 3 == 0 {
            println!("fizz");
        } else if n % 5 == 0 {
            println!("buzz");
        } else {
            println!("{n}");
        }
    }
}
1
2
fizz
4
buzz
fizz
7
8
fizz
buzz
11
fizz
13
14
fizzbuzz

The trap, and the reason this is a famous interview question: the fizzbuzz test must come first. Test % 3 first and 15 prints fizz, because an if chain stops at its first true condition (lesson 7.2). Multiples of both 3 and 5 are multiples of 15, which makes the combined test easy to write.

Next: the loop escape tools get their full story, including the one trick that's exclusive to loop.