For Loop Iteration Patterns
Teaches idiomatic for loop iteration patterns across multiple programming languages, helping you choose the right iteration style for each language and problem. Covers index-based loops, range-based iteration, iterator protocols, list comprehensions, and shell scripting loops — with BAD vs GOOD comparisons and language-specific anti-patterns.
TL;DR Checklist
- Choose the most idiomatic loop construct for the target language (e.g.,
for...ofin JS,range()in Python) - Prefer iterating over values directly instead of indices when index is not needed
- Avoid modifying a collection while iterating — collect changes and apply after
- Guard against off-by-one errors by verifying boundary conditions explicitly
- Use language-native iterators (enumerate, zip, range, iterators) before manual counters
When to Use
Use this skill when:
- Implementing iteration logic over arrays, slices, maps, or collections in any language
- Choosing between index-based loops, iterator-based loops, and functional patterns (comprehensions, map/filter)
- Converting loop logic from one language to another and seeking idiomatic equivalents
- Debugging common iteration bugs: off-by-one errors, modifying collections mid-loop, iterator invalidation
- Writing performance-sensitive loops where the iteration pattern affects cache locality or allocation
- Building data transformation pipelines that process elements sequentially
When NOT to Use
Avoid for loop patterns in these situations:
- Stream/async iteration — use async iterators, generators, or reactive streams instead of blocking for loops
- Event-driven processing — use callbacks, event emitters, or message queues rather than polling loops
- Infinite polling with busy-wait — use sleep/yield between polls or observables to avoid CPU spinning
- Complex nested iteration — consider declarative data structures (join on maps/dicts) instead of O(n²) nested loops
Core Workflow
Identify the iteration goal — Determine whether you need indices, values, both, keyed access, or parallel iteration. Checkpoint: If you only need values, skip index-based patterns and use native iterators (enumerate, for...of, range, direct iterator).
Select the language-native construct — Each language has one most-idiomatic approach:
- Python:
for item in iterableor list comprehensions - JavaScript:
for...ofover arrays; avoidfor...inon arrays - Go: single
forwithrangeover slices, maps, channels - C/C++: range-based for (C++) or index-based loop with bounds checking
- Rust: immutable
.iter(), mutable.iter_mut(), or.into_iter() - Shell:
for item in listor C-stylefor ((i=0; i<n; i++))
- Python:
Implement the iteration body — Write the core logic inside the loop. Checkpoint: Verify the loop body does not mutate the collection being iterated over unless using a deliberate safe pattern (see anti-patterns below).
Handle edge cases — Empty collections, single-element collections, and boundary conditions. Checkpoint: Add a guard clause or assert for empty input when the semantics require at least one element.
Choose aggregation style — Decide whether to accumulate results via append/collect, use a list/dict comprehension, or apply functional transforms (map/filter/fold). Checkpoint: Prefer declarative comprehensions when transforming an entire collection without early exit conditions.
Verify correctness — Test with empty input, single element, typical data, and boundary values. Checkpoint: For index-based loops, manually trace first, middle, and last iterations to confirm bounds are correct.
Implementation Patterns / Reference Guide
Pattern 1: Python for Loop
Python offers multiple iteration idioms. The core principle is "iterate over values directly; reach for indices only when you actually need them."
from typing import Any
# ✅ GOOD — iterate over values directly (most Pythonic)
def print_items(items: list[str]) -> None:
"""Print each item in the collection."""
for item in items:
print(item)
# ✅ GOOD — when index is needed, use enumerate
def indexed_display(items: list[str]) -> list[tuple[int, str]]:
"""Return a list of (index, value) pairs."""
return [(i, item) for i, item in enumerate(items)]
# ✅ GOOD — range-based iteration (useful for step or index math)
def every_second_element(values: list[float]) -> list[float]:
"""Return elements at even indices using range with a step."""
result: list[float] = []
for i in range(0, len(values), 2):
result.append(values[i])
return result
# ✅ GOOD — list comprehension (preferred over explicit loop for transforms)
def squared_positive(numbers: list[float]) -> list[float]:
"""Return squared values, filtering out negatives."""
return [n ** 2 for n in numbers if n >= 0]
# ❌ BAD — index-based loop when value iteration suffices
def bad_print_items(items: list[str]) -> None:
for i in range(len(items)): # Unnecessary len() call
print(items[i]) # Double indexing instead of direct access
# ❌ BAD — modifying collection while iterating (RuntimeError or undefined behavior)
def bad_remove_evens(values: list[int]) -> list[int]:
for v in values: # Modifying 'values' during iteration is unsafe
if v % 2 == 0:
values.remove(v) # RuntimeError or skipped elements
return values
Key Python principles:
- Prefer
for item in collectionoverrange(len(collection))— it avoids double indexing and works with any iterable (not just sequences) - Use
enumerate()when both index and value are needed - Use list/dict comprehensions for simple transforms — they are faster than explicit
for+append - Never modify a list while iterating over it; use filtering instead:
[x for x in lst if condition(x)]
Pattern 2: JavaScript for Loop
JavaScript has three loop constructs for iteration. The modern standard favors for...of and array methods over the classic C-style loop.
// ✅ GOOD — for...of iterates over values (arrays, strings, Maps, Sets)
function printItems(items) {
for (const item of items) {
console.log(item);
}
}
// ✅ GOOD — for...of with destructuring (array of objects)
function logNames(users) {
for (const { name, role } of users) {
console.log(`${name}: ${role}`);
}
}
// ✅ GOOD — traditional C-style for when you need the index
function doubleEveryOther(numbers) {
const result = [];
for (let i = 0; i < numbers.length; i++) {
if (i % 2 === 1) {
result.push(numbers[i] * 2);
} else {
result.push(numbers[i]);
}
}
return result;
}
// ✅ GOOD — for...of with break/continue for early exit
function findFirstEven(items) {
for (const item of items) {
if (item % 2 === 0) {
return item; // Early exit on first match
}
}
return null; // No even number found
}
// ❌ BAD — for...in iterates over enumerable property keys, NOT array values
function bad_forInLoop(items) {
const results = [];
for (const index in items) { // Iterates over "0", "1", "2" as strings
results.push(items[index] * 2); // index is a string! Unexpected behavior
}
return results;
}
// ❌ BAD — modifying array during for...of iteration (skipped elements)
function bad_removeFalsy(arr) {
for (const val of arr) { // Modifying arr during iteration skips items
if (!val) {
arr = arr.filter(v => v); // Creates new array, but loop still runs on old state conceptually
}
}
}
// ✅ GOOD — use array methods instead of loops for common transforms
function filterActiveUsers(users) {
return users.filter(user => user.active);
}
function transformPrices(prices, taxRate) {
return prices.map(price => price * (1 + taxRate));
}
Key JavaScript principles:
for...ofiterates over values — use this for arrays, strings, Maps, Setsfor...initerates over enumerable property keys — NEVER use it on arrays; only on plain objects- C-style
foris fine when you need the index or a custom step - Modern JS: prefer
Array.prototype.map,.filter(),.reduce()for collection transforms - Use
constinstead ofletinfor...ofsince the variable is re-assigned each iteration
Pattern 3: Go for Loop
Go has only one loop construct: for. Its flexibility comes from range over slices, maps, channels, and strings.
package main
import "fmt"
// ✅ GOOD — range over slice with index and value
func printItems(items []string) {
for i, item := range items {
fmt.Printf("%d: %s\n", i, item)
}
}
// ✅ GOOD — ignore unused index variable with blank identifier
func sumValues(numbers []int) int {
total := 0
for _, val := range numbers { // '_' discards the index
total += val
}
return total
}
// ✅ GOOD — range over map (key-value pairs)
func printMapEntries(entries map[string]int) {
for key, value := range entries {
fmt.Printf("%s = %d\n", key, value)
}
}
// ✅ GOOD — C-style for loop when you need custom control (step, reverse iteration)
func doubleEvenIndices(numbers []int) []int {
result := make([]int, len(numbers))
for i := 0; i < len(numbers); i += 2 { // Step of 2
result[i] = numbers[i] * 2
}
return result
}
// ✅ GOOD — range over channel (reads until closed)
func sumFromChannel(ch <-chan int) int {
total := 0
for val := range ch { // Reads until channel is closed
total += val
}
return total
}
// ❌ BAD — using index-based access when range provides values directly
func bad_printItems(items []string) {
for i := 0; i < len(items); i++ { // Unnecessary manual indexing
fmt.Println(items[i]) // Double lookup instead of direct value
}
}
// ❌ BAD — not handling empty slice gracefully
func bad_maxValue(numbers []int) int {
maxVal := numbers[0] // Panic on empty slice!
for _, val := range numbers {
if val > maxVal {
maxVal = val
}
}
return maxVal
}
// ✅ GOOD — handle empty input defensively
func safeMaxValue(numbers []int) (int, error) {
if len(numbers) == 0 {
return 0, fmt.Errorf("cannot find max of empty slice")
}
maxVal := numbers[0]
for _, val := range numbers[1:] {
if val > maxVal {
maxVal = val
}
}
return maxVal, nil
}
Key Go principles:
- Go's
foris the only loop construct — nowhile,do...while, orforeach for i, v := range slicegives you both index and value; use_to discard unused onesfor range chreads from a channel until it closes- C-style
foris idiomatic when you need custom increment steps (e.g., iterating by 2, reversing) - Always handle empty slices before accessing by index to avoid panics
Pattern 4: C/C++ for Loop
C uses the classic C-style loop exclusively. C++ adds range-based for loops and STL iterators.
// === C-Style (C and C++) ===
// ✅ GOOD — traditional index-based loop with bounds checking
void print_array(const int* arr, size_t len) {
if (arr == nullptr || len == 0) {
return; // Guard against null/empty
}
for (size_t i = 0; i < len; ++i) { // Use size_t to avoid signedness warnings
printf("%d\n", arr[i]);
}
}
// ✅ GOOD — reverse iteration (common pattern in C/C++)
void process_in_reverse(const double* data, size_t len) {
for (size_t i = len; i > 0; --i) { // Post-decrement stops at 0 safely
printf("%.2f\n", data[i - 1]); // Adjust index since loop ends at 1
}
}
// === C++ with STL Containers ===
#include <vector>
#include <string>
#include <map>
#include <algorithm>
// ✅ GOOD — range-based for (C++11+) iterates over values directly
void print_names(const std::vector<std::string>& names) {
for (const auto& name : names) { // const ref avoids copy
std::cout << name << '\n';
}
}
// ✅ GOOD — range-based for with structured binding (C++17+)
void print_scores(const std::map<std::string, int>& scores) {
for (const auto& [name, score] : scores) { // Structured binding
std::cout << name << ": " << score << '\n';
}
}
// ✅ GOOD — modifying elements requires mutable reference
void double_scores(std::vector<int>& scores) {
for (int& score : scores) { // Non-const ref allows modification
score *= 2;
}
}
// ✅ GOOD — iterator-based loop for STL containers (useful with erase-if pattern)
void remove_negatives(std::vector<int>& values) {
auto it = values.begin();
while (it != values.end()) {
if (*it < 0) {
it = values.erase(it); // erase returns next valid iterator
} else {
++it;
}
}
}
// ❌ BAD — using index on vector when range-based for suffices
void bad_print_vec(const std::vector<int>& vec) {
for (size_t i = 0; i < vec.size(); ++i) { // Unnecessary indexing
std::cout << vec[i] << '\n'; // Random access overhead
}
}
// ❌ BAD — erase during range-based for loop (iterator invalidation!)
void bad_remove_zeros(std::vector<int>& values) {
for (auto val : values) { // Copy, not iterator
if (val == 0) {
values.erase(/* ??? */); // Cannot safely erase here — iterators invalidated
}
}
}
// ❌ BAD — signed/unsigned comparison warning
void bad_signed_loop(const std::vector<int>& data) {
for (int i = 0; i < data.size(); ++i) { // int vs size_t comparison!
printf("%d\n", data[i]);
}
}
Key C/C++ principles:
- In C, the
forloop is your only iteration tool — manage bounds and increments manually - Always use
size_tfor indices into arrays/vectors (avoid signed/unsigned mismatch) - C++ range-based for loops (
for (const auto& x : container)) are idiomatic and avoid manual indexing - When erasing from a vector, use the iterator pattern (Erase–Remove Idiom or explicit iterator tracking) — never erase during a range-based loop
- Use
const auto&in range-for to avoid copying large objects
Pattern 5: Rust for Loop
Rust provides multiple iteration strategies with different ownership semantics. Choosing the right one prevents unnecessary allocations and borrow checker errors.
// ✅ GOOD — immutable iteration via .iter() (borrows elements)
fn print_items(items: &[String]) {
for item in items.iter() { // Borrows each element immutably
println!("{item}");
}
}
// ✅ GOOD — concise range-based iteration (Rust auto-delegates to .into_iter())
fn sum_numbers(numbers: &[i32]) -> i32 {
let mut total = 0;
for &num in numbers { // Destructures by value (i32 is Copy)
total += num;
}
total
}
// ✅ GOOD — mutable iteration via .iter_mut()
fn double_values(values: &mut Vec<i32>) {
for val in values.iter_mut() { // Borrows each element mutably
*val *= 2;
}
}
// ✅ GOOD — enumerate (index + value) with proper destructuring
fn indexed_display(items: &[String]) -> Vec<(usize, &String)> {
items.iter().enumerate().collect() // Returns [(0, &"a"), (1, &"b"), ...]
}
// ✅ GOOD — zip combines two iterables side by side
fn add_vectors(a: &[f64], b: &[f64]) -> Vec<f64> {
a.iter().zip(b.iter())
.map(|(&x, &y)| x + y)
.collect()
}
// ✅ GOOD — into_iter() consumes the collection (moves elements out)
fn consume_and_uppercase(list: Vec<String>) -> Vec<String> {
let mut result = Vec::new();
for mut s in list.into_iter() { // Takes ownership of each String
s.make_ascii_uppercase(); // Can mutate since we own the string
result.push(s);
}
result
}
// ✅ GOOD — iterator chain with filter and map (functional style)
fn positive_squares(numbers: &[i32]) -> Vec<i32> {
numbers.iter()
.filter(|&&n| n > 0) // Filter out negatives
.map(|&n| n * n) // Square each remaining value
.collect() // Collect into Vec<i32>
}
// ❌ BAD — indexing in a loop when iteration suffices (panics on out-of-bounds!)
fn bad_print_items(items: &[String]) {
for i in 0..items.len() { // Unsafe index access
println!("{}", items[i]); // Can panic if len == 0 and we access beyond
}
}
// ❌ BAD — collecting into a new Vec inside an explicit loop when iterator methods exist
fn bad_double_values(values: &[i32]) -> Vec<i32> {
let mut result = Vec::new();
for i in 0..values.len() { // Index-based with manual accumulation
result.push(values[i] * 2); // Unnecessary allocation overhead
}
result
}
Key Rust principles:
.iter()— borrows elements immutably (most common, most flexible).iter_mut()— borrows elements mutably (use when you need to modify in place).into_iter()— takes ownership (move semantics), consumes the collection- For primitive
Copytypes (i32,f64, bool),for val in sliceautomatically moves/copies values - Use iterator adapters (
.map(),.filter(),.zip(),.enumerate()) for composable, zero-cost-abstraction pipelines - Indexing with
[i]can panic; prefer iterators unless you specifically need random access
Pattern 6: Shell Scripting for Loop
Shell for loops iterate over word-split lists. They handle files, variables, and command output. Understanding word splitting is critical for correctness.
#!/usr/bin/env bash
# Shell for-loop patterns — POSIX-compatible with bash extensions noted
# ✅ GOOD — iterating over a glob pattern (filenames)
function list_text_files() {
local directory="${1:-.}"
for filepath in "$directory"/*.txt; do # Quotes protect spaces in filenames
if [[ -f "$filepath" ]]; then # Guard against empty glob (literal "*.txt")
echo "Found: $filepath"
fi
done
}
# ✅ GOOD — iterating over a list of items (word-split safely)
function process_items() {
local items=("alpha" "beta" "gamma")
for item in "${items[@]}"; do # Array expansion preserves each element
echo "Processing: $item"
done
}
# ✅ GOOD — iterating over command output (read line by line)
function count_lines_in_files() {
local directory="${1:-.}"
while IFS= read -r filepath; do # Read avoids word splitting and trailing newlines
if [[ -f "$filepath" ]]; then
wc -l < "$filepath" | xargs echo " Lines:" "$filepath"
fi
done < <(find "$directory" -maxdepth 1 -name "*.txt" -type f)
}
# ✅ GOOD — C-style arithmetic loop (bash-specific with double parentheses)
function sum_range() {
local limit="${1:-10}"
local total=0
for (( i = 1; i <= limit; i++ )); do # Integer arithmetic, no subshell needed
(( total += i ))
done
echo "$total"
}
# ✅ GOOD — iterating over range in bash 4.0+ (brace expansion)
function print_numbers() {
local start="${1:-1}"
local end="${2:-5}"
for i in $(seq "$start" "$end"); do # seq generates the sequence
echo "$i"
done
}
# ❌ BAD — unquoted variable expansion (word splitting breaks on spaces!)
function bad_process_names() {
local name="John Doe"
for word in $name; do # Splits "John Doe" into "John" and "Doe"!
echo "Name: $word" # Outputs two lines instead of one
done
}
# ❌ BAD — iterating over glob without checking for no-match (literal pattern)
function bad_list_files() {
for filepath in /nonexistent/*.xyz; do # If no match, loop runs once with literal string
echo "File: $filepath" # Outputs: "File: /nonexistent/*.xyz"
done
}
# ✅ GOOD — handle no-match glob explicitly
function safe_list_files() {
local pattern="/nonexistent/*.xyz"
shopt -s nullglob # Disable literal expansion on no match
for filepath in $pattern; do
echo "File: $filepath"
done
shopt -u nullglob # Restore default behavior
}
Key Shell principles:
- Always quote variables in
forloops:"$var"not$var— unquoted variables undergo word splitting and glob expansion - Use
"${array[@]}"to iterate over array elements without splitting - Globs that match nothing expand to the literal pattern string — use
nullglobor check file existence with[[ -f ]] - C-style
for (( ))is a bash extension, not POSIX; it handles integer arithmetic natively - For command output, prefer
while read -r lineoverfor line in $(command)to handle spaces and newlines correctly
Constraints
MUST DO
- Always prefer iterating over values directly rather than using manual index counters when the index is not needed
- Guard against empty collections before accessing elements by index (add null/empty checks)
- Use language-native iterator constructs (enumerate, for...of, range, .iter(), "${array[@]}") instead of manual counter loops
- When erasing or removing elements from a collection during iteration, use the safe pattern: collect deletions first, then remove; or use the language's built-in filter/retain methods
- Use
const/final/ immutable bindings in loop variables when the value should not be reassigned - Handle the no-match case for glob patterns in shell scripts (use
nullglobor test existence)
MUST NOT DO
- Modify a collection while iterating over it with a forward loop — this causes skipped elements, double-processing, or crashes depending on the language
- Use index-based loops to access every element when value iteration is available — it doubles the work (length calculation + indexing) and introduces off-by-one risks
- Use
for...inon JavaScript arrays — it iterates over enumerable property keys (including inherited ones), not array values - Access elements by index in Rust without bounds checking (
[i]panics; use.get(i)for safe access or prefer iterators) - Loop over
$(command_output)in shell when filenames may contain spaces — usewhile read -rinstead - Use
range(len(...))in Python when you only need values — it generates unnecessary integer objects and forces double lookup - Ignore the return value of iterator operations that can fail (e.g., Rust's
.get(), Go's map access with two-return syntax)
Output Template
When helping with loop-related questions, produce output following this structure:
Language Context — Identify the programming language and version constraints (e.g., "Python 3.9+", "JavaScript ES6+"). State whether there are any compatibility concerns (e.g., bash
for (( ))is not POSIX).Iteration Style — Recommend the most idiomatic iteration pattern for the problem, explaining why it is preferred over alternatives. Include reasoning about readability, performance, and safety.
Code Example — Provide a complete, working code example using proper typing (where applicable), meaningful variable names, and docstrings/comments. Include both the primary pattern and the BAD anti-pattern to contrast.
Edge Cases — List notable gotchas specific to this language and pattern:
- Empty collection behavior
- Mutating during iteration safety
- Off-by-one boundary conditions
- Shell-specific concerns (word splitting, glob no-match)
- Iterator invalidation risks (C++/Rust)
Related Skills
| Skill | Purpose |
|---|---|
algorithms |
Algorithm complexity analysis for loop-heavy code |
sorting-algorithms |
Common sorting implementations use nested loops |
Live References
Authoritative documentation links for this skill's domain. The model follows markdown links to resolve external references and inline content.