---
title: "7.5 for loops and ranges"
description: "Rust's for loop over ranges: half-open and inclusive ranges, rev and step_by, and why there's no three-part C-style for."
url: "https://learnrust.net/chapter-7/for-loops-and-ranges/"
last_updated: "2026-06-12"
---

# 7.5 for loops and ranges

Chapter 4 ended with a debt. The falling-ball program ([4.x](https://learnrust.net/chapter-4/chapter-4-summary-and-quiz/)) closed on six lines you were instructed to find irritating:

```rust
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:

```rust
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](https://learnrust.net/chapter-4/numeric-conversions/) 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**:

```rust
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:

```rust
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?

```rust
fn main() {
    for n in (0..10).step_by(3) {
        print!("{n} ");
    }
    println!();
    for n in (1..=3).rev() {
        print!("{n} ");
    }
    println!();
}
```

<details class="solution">
<summary>Show solution</summary>

```
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.

</details>

**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).

<details class="solution">
<summary>Show solution</summary>

```rust
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."

</details>

**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).

<details class="solution">
<summary>Show solution</summary>

```rust
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.

</details>

**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`.

<details class="solution">
<summary>Show solution</summary>

```rust
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.

</details>

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

## Sitemap

See the full [sitemap](https://learnrust.net/sitemap.md) for all pages.
