for Loops and Iterators

Published by Programming Line in Ruby. Last updated .

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 (with for) or each block parameter (with each).
  • collection — any object that responds to each, most commonly an Array, Hash, or Range.
  • do...end or { ... } — the block passed to an iterator method; both are equivalent, though { } is conventionally used for short, single-line blocks and do...end for 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 map or select; each ignores 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 each method on collection, passing the block along with the call.
  • 2. Inside each, for every element, Ruby executes yield (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 (as map and select do) or discard it (as plain each does).
  • 4. Once every element has been yielded, each returns — 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 other Enumerable methods) over for in idiomatic Ruby; reserve for for the rare case where you deliberately want the loop variable to survive past the loop.
  • Reach for the most specific iterator for the job: map when transforming, select/reject when filtering, reduce when combining into one value, rather than building up results manually inside a plain each.
  • 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 return nil if nothing changed — don’t chain another call directly onto them inside a loop.
  • Use break to stop early once you have what you need, rather than iterating over an entire large collection needlessly.
  • Keep in mind that only nil and false are falsy in Ruby — inside a block, a guard like next if some_count will run for every value including 0 and "", which surprises people coming from languages where those are falsy.

Practice Exercises

  • Write a for loop that prints the 5-times multiplication table from 5 x 1 up to 5 x 10, one line per row.
  • Given words = ["ruby", "python", "elixir"], use each_with_index to print each word alongside its position, starting the count at 1 instead of 0 (hint: add 1 inside the block).
  • Use reduce to compute the sum of the squares of the numbers 1 through 10 (expected result: 385), then rewrite the same computation using map followed by sum.

Summary

  • for variable in collection loops over a collection using each internally, but does not create a new scope — the loop variable persists after the loop.
  • Iterator methods like each, map, and select take a block and are built on top of each via the Enumerable module.
  • 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.
  • each always returns its original receiver, ignoring the block’s return value; use map when you need a transformed array back.
  • Never mutate a collection while iterating over it — indices shift and elements get silently skipped.
  • next skips to the next iteration; break exits the loop immediately.