for Loops and Iterators
A for loop repeats a block of code once for each element in a collection, using syntax borrowed from older, C-like languages. Ruby supports it, but idiomatic Ruby code almost never uses it — instead, Ruby programmers reach for iterators: methods like each, map, and select that take a block of code and run it for every item. Understanding both, and exactly how they differ under the hood, is essential to reading and writing real Ruby.
Overview: How Iteration Works in Ruby
In many languages, looping is a language keyword: for, while, and so on. Ruby has those keywords too, but its real iteration model is built on objects and blocks. A block is a chunk of code, written with do...end or {...}, that you attach to a method call. An iterator is simply a method that yields control to that block, once per item, instead of the caller manually stepping through indices.
Every built-in collection — Array, Hash, Range, and more — implements a method called each, and mixes in the Enumerable module, which builds dozens of higher-level iterators (map, select, reduce, sort_by, and so on) entirely on top of that single each method. This is why any custom class that defines its own each and includes Enumerable instantly gains all of those methods for free — a great example of how far Ruby’s “everything is an object, behavior comes from modules” design goes.
The for loop is different in one crucial way: it does not open a new variable scope. Ruby’s for variable in collection loop uses each internally to walk the collection, but the loop variable, and any variable first assigned inside the loop body, belongs to the enclosing scope, not the loop. It is still alive and holds its last value after the loop finishes. A block passed to each, by contrast, always introduces its own local scope: block parameters shadow any outer variable of the same name, and any variable created for the first time inside the block disappears once the block ends. This one difference is the main reason the Ruby community strongly favors each and friends over for — the scoping behavior of iterators is more predictable and avoids accidentally leaking variables or overwriting ones you already had.
Syntax
for variable in collection
# loop body -- 'variable' is also readable after the loop ends
end
collection.each do |variable|
# block body -- 'variable' exists only inside this block
end
variable— the name bound to each element in turn (withfor) or each block parameter (witheach).collection— any object that responds toeach, most commonly anArray,Hash, orRange.do...endor{ ... }— the block passed to an iterator method; both are equivalent, though{ }is conventionally used for short, single-line blocks anddo...endfor multi-line ones.- The value of the last expression inside a block is what gets returned to the iterator for methods that use it, such as
maporselect;eachignores the block’s return value entirely and returns the original collection.
Common iterator methods you will use constantly:
| Method | What it does |
|---|---|
each |
Runs the block once per element; returns the original collection. |
each_with_index |
Yields [element, index] pairs. |
map / collect |
Builds and returns a new array of the block’s return values. |
select / filter |
Returns a new array of elements for which the block is truthy. |
reject |
Returns a new array of elements for which the block is falsy. |
reduce / inject |
Combines all elements into a single value using an accumulator. |
times |
Runs the block n times, yielding 0 up to n - 1. |
upto / downto |
Counts up or down between two integers, inclusive. |
step |
Iterates over a numeric range with a custom increment. |
Examples
Example 1: A basic for loop
for i in 1..5
puts "Number: #{i}"
end
puts "i is still accessible: #{i}"
Output:
Number: 1
Number: 2
Number: 3
Number: 4
Number: 5
i is still accessible: 5
The loop runs once for each integer in the range 1..5. Notice the last line: after the loop ends, i is still defined and holds 5, its final value. That is because for does not create a new scope — it reuses whatever scope it was written in.
Example 2: each with block-local scope
fruits = ["apple", "banana", "cherry"]
fruits.each do |fruit|
puts "I like #{fruit}"
end
begin
puts fruit
rescue NameError
puts "fruit is not defined outside the block, as expected"
end
Output:
I like apple
I like banana
I like cherry
fruit is not defined outside the block, as expected
This time, fruit is a block parameter, so it only exists while the block is running. Trying to reference it afterward raises a NameError, which the example catches to show the point clearly. This is the safer, more predictable behavior that makes each preferable to for in almost all real Ruby code.
Example 3: A realistic iteration over a Hash
scores = { "Alice" => 92, "Bob" => 85, "Charlie" => 78 }
scores.each do |name, score|
status = score >= 90 ? "A" : score >= 80 ? "B" : "C"
puts "#{name}: #{score} (#{status})"
end
total = scores.values.reduce(0) { |sum, score| sum + score }
average = total / scores.size.to_f
puts "Average score: #{average.round(2)}"
Output:
Alice: 92 (A)
Bob: 85 (B)
Charlie: 78 (C)
Average score: 85.0
Iterating a Hash with each yields each [key, value] pair, which the block destructures into name and score. The second part of the example shows a different iterator, reduce, folding every value in the hash down into a single running total — a pattern you will use constantly once you get comfortable with iterators.
How It Works Step by Step
When Ruby evaluates collection.each do |x| ... end, it does the following, in order:
- 1. It calls the
eachmethod oncollection, passing the block along with the call. - 2. Inside
each, for every element, Ruby executesyield(or an equivalent internal call), which jumps into the block, binding the current element to the block’s parameter(s). - 3. The block runs to completion; its final expression’s value is handed back to
each, which may use it (asmapandselectdo) or discard it (as plaineachdoes). - 4. Once every element has been yielded,
eachreturns — by convention, returning the original collection it was called on.
A for loop follows the same underlying mechanism — it still needs the collection to respond to each — but the language handles the variable binding differently: instead of creating a fresh scope for the loop variable the way a block does, it reuses the surrounding scope. That is the entire difference; everything else about how the elements get visited is the same.
break and next control the flow inside either kind of loop: next skips to the following iteration, and break exits the loop entirely.
numbers = [3, 7, 12, 5, 18, 2]
numbers.each do |n|
next if n.odd?
break if n > 15
puts "Even and small: #{n}"
end
Output:
Even and small: 12
Ruby walks the array in order: 3 and 7 are odd, so next skips them. 12 is even and not greater than 15, so it gets printed. 5 is odd and skipped. 18 is even but greater than 15, so break stops the loop before it ever reaches 2.
Common Mistakes
Mistake 1: Expecting each to transform the array
New Rubyists often reach for each when they actually want the block’s return values collected into a new array:
numbers = [1, 2, 3]
doubled = numbers.each { |n| n * 2 }
puts doubled.inspect
Output:
[1, 2, 3]
each always returns the original collection, completely ignoring what the block evaluates to, so doubled ends up identical to numbers. To build a new, transformed array, use map instead:
numbers = [1, 2, 3]
doubled = numbers.map { |n| n * 2 }
puts doubled.inspect
Output:
[2, 4, 6]
Mistake 2: Mutating a collection while iterating over it
Deleting or adding elements to an array from inside its own each loop silently corrupts the iteration, because each tracks a numeric index against the array’s current length as it goes:
numbers = [1, 2, 4, 5]
numbers.each do |n|
numbers.delete(n) if n.even?
end
puts numbers.inspect
Output:
[1, 4, 5]
You might expect every even number to be removed, but 4 survives: once 2 is deleted, every later element shifts one position to the left, and the loop’s index skips right over 4 without ever checking it. The fix is to never mutate a collection you are iterating — build a new one instead with a non-mutating method like reject:
numbers = [1, 2, 4, 5]
numbers = numbers.reject(&:even?)
puts numbers.inspect
Output:
[1, 5]
Mistake 3: Assuming a for loop variable is local
Because for does not open a new scope, it can silently overwrite a variable you already had:
i = "not a number"
for i in 1..3
puts i
end
puts i
Output:
1
2
3
3
The original string stored in i is gone — the loop reused and overwrote the same variable. Using each with a block avoids this entirely, since the block parameter is always local to the block:
i = "not a number"
(1..3).each { |i| puts i }
puts i
Output:
1
2
3
not a number
The outer i survives untouched, because the block’s |i| shadows it only for the duration of the block.
Best Practices
- Prefer
each(and otherEnumerablemethods) overforin idiomatic Ruby; reserveforfor the rare case where you deliberately want the loop variable to survive past the loop. - Reach for the most specific iterator for the job:
mapwhen transforming,select/rejectwhen filtering,reducewhen combining into one value, rather than building up results manually inside a plaineach. - Never add or remove elements from a collection you are actively iterating over; iterate over a copy (
collection.dup) or build a new collection instead. - Remember that
sort!,map!, and other bang methods mutate in place and can returnnilif nothing changed — don’t chain another call directly onto them inside a loop. - Use
breakto stop early once you have what you need, rather than iterating over an entire large collection needlessly. - Keep in mind that only
nilandfalseare falsy in Ruby — inside a block, a guard likenext if some_countwill run for every value including0and"", which surprises people coming from languages where those are falsy.
Practice Exercises
- Write a
forloop that prints the 5-times multiplication table from5 x 1up to5 x 10, one line per row. - Given
words = ["ruby", "python", "elixir"], useeach_with_indexto print each word alongside its position, starting the count at 1 instead of 0 (hint: add 1 inside the block). - Use
reduceto compute the sum of the squares of the numbers 1 through 10 (expected result:385), then rewrite the same computation usingmapfollowed bysum.
Summary
for variable in collectionloops over a collection usingeachinternally, but does not create a new scope — the loop variable persists after the loop.- Iterator methods like
each,map, andselecttake a block and are built on top ofeachvia theEnumerablemodule. - Block parameters are always local to the block, shadowing any outer variable with the same name — this predictability is the main reason Ruby style favors iterators over
for. eachalways returns its original receiver, ignoring the block’s return value; usemapwhen you need a transformed array back.- Never mutate a collection while iterating over it — indices shift and elements get silently skipped.
nextskips to the next iteration;breakexits the loop immediately.
