for Loops and Ranges

A for loop repeats a block of code once for every element produced by something iterable — a range of numbers, a list, an array, a string’s characters, or a map’s entries. Kotlin has no C-style for (int i = 0; i < n; i++) loop at all; instead, it leans on ranges like 1..5 or 0 until n, which read closer to plain English and remove an entire class of off-by-one bugs when used correctly. Because this syntax shows up constantly — counting, iterating collections, building grids, retrying operations — getting comfortable with it here pays off throughout the rest of the language.

Overview: How Kotlin’s for Loop and Ranges Work

Kotlin’s for loop only knows how to do one thing: iterate over something that exposes an iterator() function returning a Kotlin Iterator. Arrays, List, Set, Map.entries, and String (as a sequence of characters) all qualify already. Ranges qualify too, because a range like 1..5 is not special syntax that the loop understands directly — it is an actual object. Writing 1..5 creates an IntRange (there are also LongRange and CharRange for other types), and IntRange implements Iterable<Int>. The for loop just calls .iterator() on whatever you give it and calls hasNext()/next() until it’s exhausted, exactly like Java’s enhanced for-loop.

There is one important performance detail worth knowing: for a plain integer range with no custom step (for (i in 1..1000000)), the Kotlin compiler recognizes the pattern and compiles it down to a primitive counter loop with a comparison and increment — no IntRange object or boxed Iterator is actually allocated at runtime. You get the readability of range syntax with the performance of a hand-written counting loop. This optimization only kicks in for the literal loop-header shape, so don’t worry about memorizing when it applies — just know that ranges in for loops are cheap in practice.

The loop variable introduced by for (x in ...) is implicitly a read-only binding, similar to declaring it with val. You cannot reassign it inside the loop body — Kotlin deliberately removes the mutable-loop-counter foot-gun that C-style loops invite.

Syntax

The general form is:

for (element in numbers) {
    println(element)
}

Where numbers can be a range, an array, a List, a String, a Map‘s entries (destructured as a pair), or any custom type that provides an iterator. The table below covers the range-building operators you will use inside the parentheses:

Operator / function Meaning Example
.. Inclusive range, ascending, step 1 1..5 → 1,2,3,4,5
until Exclusive upper bound, ascending 0 until 5 → 0,1,2,3,4
..< Newer operator form of until (exclusive upper bound) 0..<5 → 0,1,2,3,4
downTo Descending range, step 1 by default 5 downTo 1 → 5,4,3,2,1
step n Changes the increment magnitude (must be positive) 1..10 step 3 → 1,4,7,10

All of these produce a progression object (IntProgression, CharProgression, and so on) that the for loop then iterates.

Examples

Example 1: A basic ascending range

fun main() {
    for (i in 1..5) {
        println(i)
    }
}

Output:

1
2
3
4
5

The range 1..5 is inclusive on both ends, so the loop visits 1, 2, 3, 4, and 5 — five iterations total. This is the most common shape you’ll write for straightforward counting.

Example 2: Counting down with a step

fun main() {
    for (i in 10 downTo 1 step 2) {
        println(i)
    }
}

Output:

10
8
6
4
2

downTo builds a descending progression starting at 10 and ending at (or before) 1, and step 2 makes it move two at a time instead of one. Note that step always takes a positive magnitude — the direction (ascending vs. descending) is entirely determined by whether you wrote ../until or downTo.

Example 3: Iterating a list with its index

fun main() {
    val fruits = listOf("apple", "banana", "cherry")
    for ((index, fruit) in fruits.withIndex()) {
        println("$index: $fruit")
    }
}

Output:

0: apple
1: banana
2: cherry

withIndex() wraps each element together with its position into an IndexedValue, which the for loop destructures directly into index and fruit using the parentheses pattern (index, fruit). This is the idiomatic replacement for a manual counter variable when you need both the position and the value.

How It Works Step by Step

Under the hood, every for (x in y) { body } desugars to roughly the same shape regardless of what y is: Kotlin calls y.iterator() once to get an Iterator, then repeatedly checks hasNext() and, while it returns true, calls next() to produce the next value of x and runs body. This is why anything with an iterator() function — not just collections and ranges — can be used in a for loop, including a plain String, which iterates its characters one at a time:

val word = "Kotlin"
for (c in word) {
    print(c)
}
println()

Output:

Kotlin

Each character of the string is produced by the string’s own iterator, printed with no separator via print, and the trailing println() just adds the final newline. The important mental model: the loop header decides what to iterate and in what order; the loop body runs once per produced element, and the whole loop finishes as soon as hasNext() reports false.

Common Mistakes

Mistake 1: Using .. when you meant an exclusive bound

A classic off-by-one bug is using an inclusive range where a collection’s valid indices are meant to be exclusive of its size:

fun main() {
    val items = listOf("a", "b", "c")
    for (i in 0..items.size) {
        println(items[i])
    }
}

Output:

a
b
c
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index 3 out of bounds for length 3

items.size is 3, but valid indices only go up to 2. 0..items.size includes 3, so the loop tries items[3] and crashes. The fix is to use items.indices (or 0 until items.size), which already excludes the out-of-range endpoint:

fun main() {
    val items = listOf("a", "b", "c")
    for (i in items.indices) {
        println(items[i])
    }
}

Output:

a
b
c

Mistake 2: Trying to reassign the loop variable

Because the loop variable behaves like an implicit val, attempting to mutate it directly is a compile error, not a runtime surprise:

for (i in 1..5) {
    i++
    println(i)
}

This fails to compile with “val cannot be reassigned”. If you genuinely need a counter that can be adjusted mid-loop (for example, skipping ahead), don’t fight the for loop — use a separate var and a while loop instead, or restructure the logic to avoid needing to mutate the position.

Mistake 3: Giving step a negative number

It’s tempting to think a negative step controls direction, but step only ever accepts a positive magnitude — direction comes from ../until versus downTo:

fun main() {
    for (i in 10 downTo 1 step -2) {
        println(i)
    }
}

Output:

Exception in thread "main" java.lang.IllegalArgumentException: Step must be positive, was: -2.

The progression validates its step eagerly when it’s constructed, so the program crashes before printing anything at all. Since the range is already descending via downTo, the fix is simply to use a positive step:

for (i in 10 downTo 1 step 2) {
    println(i)
}

Output:

10
8
6
4
2

Best Practices

  • Prefer until (or ..<) over 0..size - 1 when you need an exclusive upper bound — it’s clearer and avoids off-by-one arithmetic entirely.
  • Use collection.indices instead of 0 until collection.size when looping by index — it says exactly what you mean and stays correct if the collection type changes.
  • Use withIndex() when you need both the index and the value; reach for a plain for (item in collection) when you only need the value.
  • If you’re not using the index or the element at all and just want side effects per item, collection.forEach { ... } is often more idiomatic than a for loop.
  • Never try to reassign a for loop’s iteration variable — if you need a mutable running value, declare a separate var outside or below the loop header.
  • Remember step is always positive; let downTo handle descending direction.
  • For very large integer ranges in hot code paths, don’t hesitate to use a plain for (i in a..b) — the compiler optimizes simple integer ranges into a primitive counter loop, so it’s not slower than a manual index loop.

Practice Exercises

  • Write a program that prints the 5-times multiplication table from 1 to 10 (i.e. 5 x 1 = 5 through 5 x 10 = 50) using a single for loop over a range.
  • Using step, print every even number from 2 to 20 inclusive on one line separated by spaces. Hint: build the string with joinToString over a range, or accumulate with print and a space.
  • Given val names = listOf("Ann", "Bo", "Cy", "Dee"), use for with withIndex() to print each name preceded by its 1-based position, e.g. 1: Ann. Expected final line: 4: Dee.

Summary

  • Kotlin has no C-style for(;;) loop; for (x in y) iterates anything exposing an iterator(), including ranges, arrays, collections, and strings.
  • .. is inclusive, until/..< is exclusive on the upper bound, downTo descends, and step changes the increment (always as a positive number).
  • The loop variable behaves like an implicit val and cannot be reassigned inside the body.
  • Use collection.indices or withIndex() instead of hand-rolled counters to avoid off-by-one mistakes like looping through 0..size.
  • Simple integer ranges in for loops are compiled to efficient primitive counter loops, so idiomatic range syntax carries no performance penalty over a manual loop.