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..<) over0..size - 1when you need an exclusive upper bound — it’s clearer and avoids off-by-one arithmetic entirely. - Use
collection.indicesinstead of0 until collection.sizewhen 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 plainfor (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 aforloop. - Never try to reassign a
forloop’s iteration variable — if you need a mutable running value, declare a separatevaroutside or below the loop header. - Remember
stepis always positive; letdownTohandle 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 = 5through5 x 10 = 50) using a singleforloop over a range. - Using
step, print every even number from 2 to 20 inclusive on one line separated by spaces. Hint: build the string withjoinToStringover a range, or accumulate withprintand a space. - Given
val names = listOf("Ann", "Bo", "Cy", "Dee"), useforwithwithIndex()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 aniterator(), including ranges, arrays, collections, and strings. ..is inclusive,until/..<is exclusive on the upper bound,downTodescends, andstepchanges the increment (always as a positive number).- The loop variable behaves like an implicit
valand cannot be reassigned inside the body. - Use
collection.indicesorwithIndex()instead of hand-rolled counters to avoid off-by-one mistakes like looping through0..size. - Simple integer ranges in
forloops are compiled to efficient primitive counter loops, so idiomatic range syntax carries no performance penalty over a manual loop.
