Codetail

Article 9 of 15

Loops

Iteration patterns, comprehensions, generators.

25 min read

For loops

A for loop in Python iterates over any iterable: a list, string, tuple, dict, set, range, file, or any object that implements __iter__. There is no index by default. If you need one, use enumerate().

Python
1# Iterating a list
2fruits = ["apple", "banana", "cherry"]
3for fruit in fruits:
4 print(fruit)
5
6# Iterating a string
7for char in "Python":
8 print(char, end=" ") # P y t h o n
9print()
10
11# Iterating a dict, gives keys by default
12scores = {"Alice": 92, "Bob": 78}
13for name in scores:
14 print(name, scores[name])

range()

range() generates integers on demand. It does not build a list, it produces one number at a time. Use it when you need to repeat something N times or loop over numeric indices.

Python
1# range(stop), 0 up to stop-1
2for i in range(5):
3 print(i, end=" ") # 0 1 2 3 4
4print()
5
6# range(start, stop), start up to stop-1
7for i in range(2, 8):
8 print(i, end=" ") # 2 3 4 5 6 7
9print()
10
11# range(start, stop, step)
12for i in range(0, 20, 5):
13 print(i, end=" ") # 0 5 10 15
14print()
15
16# Countdown
17for i in range(5, 0, -1):
18 print(i, end=" ") # 5 4 3 2 1
19print()

break and continue

Python
1# break, exit the loop immediately
2for n in range(10):
3 if n == 5:
4 break
5 print(n, end=" ") # 0 1 2 3 4
6print()
7
8# continue, skip the rest of this iteration
9for n in range(10):
10 if n % 2 == 0:
11 continue
12 print(n, end=" ") # 1 3 5 7 9
13print()

for / else

A for loop can have an else clause. It runs if and only if the loop completed without hitting a break. This is the cleanest way to express the "search and report if not found" pattern.

Python
1def find_prime(numbers):
2 for n in numbers:
3 if all(n % i != 0 for i in range(2, n)):
4 print(f"First prime: {n}")
5 break
6 else:
7 print("No primes found")
8
9find_prime([4, 6, 8, 7, 10]) # First prime: 7
10find_prime([4, 6, 8, 10]) # No primes found

While loops

A while loop runs as long as its condition is true. Use it when you do not know how many iterations you need: user input, polling a state, reading until a sentinel value. When you know the count or have a sequence, a for loop is almost always cleaner.

Python
1# Count down
2n = 5
3while n > 0:
4 print(n, end=" ")
5 n -= 1
6print() # 5 4 3 2 1
7
8# Consume a queue
9from collections import deque
10queue = deque([1, 2, 3, 4])
11while queue:
12 item = queue.popleft()
13 print(item, end=" ") # 1 2 3 4
14print()

break and while / else

Python
1# Retry loop with break
2import random
3random.seed(42)
4
5attempts = 0
6while True:
7 attempts += 1
8 value = random.randint(1, 10)
9 if value == 7:
10 print(f"Found 7 after {attempts} attempts")
11 break
12
13# while / else, else runs only if no break occurred
14n = 10
15while n > 0:
16 if n == 3:
17 print("stopped at 3")
18 break
19 n -= 1
20else:
21 print("counted all the way down") # not printed here

Infinite loops with controlled exit

while True with a break is the canonical pattern for event loops, REPLs, and retry logic. It is more readable than duplicating the exit condition.

Python
1# Read lines until empty input
2lines = []
3# In real code: line = input("Enter text (blank to stop): ")
4# Simulated here:
5inputs = ["hello", "world", "", "ignored"]
6idx = 0
7while True:
8 line = inputs[idx]; idx += 1
9 if not line:
10 break
11 lines.append(line)
12
13print(lines) # ['hello', 'world']
14
15# Exponential backoff retry
16import time
17
18def fetch_with_retry(url, max_retries=3):
19 delay = 1
20 for attempt in range(max_retries):
21 try:
22 # result = requests.get(url)
23 return "success"
24 except Exception:
25 if attempt == max_retries - 1:
26 raise
27 time.sleep(delay)
28 delay *= 2

enumerate and zip

Two built-in functions that make loops cleaner. enumerate() gives you both the index and value without manual counter management. zip() pairs items from two or more iterables.

enumerate()

Python
1fruits = ["apple", "banana", "cherry"]
2
3# Without enumerate, error-prone counter
4for i in range(len(fruits)):
5 print(i, fruits[i])
6
7# With enumerate, cleaner and safer
8for i, fruit in enumerate(fruits):
9 print(i, fruit)
10
11# Start index at 1
12for i, fruit in enumerate(fruits, start=1):
13 print(f"{i}. {fruit}")

zip()

zip() stops at the shortest iterable. If you need to continue to the longest and fill missing values with a default, use itertools.zip_longest().

Python
1names = ["Alice", "Bob", "Carol"]
2scores = [92, 78, 85]
3grades = ["A", "C+", "B"]
4
5# Pair two lists
6for name, score in zip(names, scores):
7 print(f"{name}: {score}")
8
9# Three lists at once
10for name, score, grade in zip(names, scores, grades):
11 print(f"{name}: {score} ({grade})")
12
13# zip stops at the shortest
14short = [1, 2]
15long = [10, 20, 30, 40]
16print(list(zip(short, long))) # [(1, 10), (2, 20)]
17
18# zip_longest fills missing values
19from itertools import zip_longest
20print(list(zip_longest(short, long, fillvalue=0)))
21# [(1, 10), (2, 20), (0, 30), (0, 40)]

Combining enumerate and zip

Python
1fruits = ["apple", "banana", "cherry", "date"]
2prices = [1.20, 0.50, 3.00, 4.50]
3
4for i, (fruit, price) in enumerate(zip(fruits, prices), start=1):
5 print(f"{i}. {fruit}, {price:.2f}")

See every iteration pattern side by side below.

Iteration Mode Explorer
Pick a pattern
for fruit in fruits:
iterfruit
0apple
1banana
2cherry
3date
4elderberry

Comprehensions

Comprehensions are compact loop expressions that produce a collection. Python has three kinds: list, dict, and set. All follow the same structure: expression first, then the loop, then an optional filter.

Python
1numbers = range(10)
2
3# List comprehension: [expression for item in iterable if condition]
4evens = [x for x in numbers if x % 2 == 0]
5squares = [x ** 2 for x in numbers]
6print(evens) # [0, 2, 4, 6, 8]
7print(squares) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
8
9# Dict comprehension: {key: value for item in iterable}
10sq_map = {x: x ** 2 for x in range(6)}
11print(sq_map) # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
12
13# Set comprehension: {expression for item in iterable}
14words = ["hello", "world", "hello", "python"]
15lengths = {len(w) for w in words}
16print(lengths) # {5, 6}

Nested comprehensions

Multiple for clauses are written in the same order as nested loops: outer first, inner second.

Python
1# Flatten a 2D list
2matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
3flat = [x for row in matrix for x in row]
4print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
5
6# Cartesian product
7sizes = ["S", "M", "L"]
8colors = ["red", "blue"]
9variants = [(s, c) for s in sizes for c in colors]
10print(variants)

When not to use a comprehension

Comprehensions lose their readability advantage as complexity grows. If the expression or condition is long, or if you need side effects, a plain loop communicates intent more clearly.

Python
1# Too complex, use a plain loop
2result = [
3 transform(item)
4 for item in get_items()
5 if item.is_valid()
6 if item.price < threshold
7]
8
9# Clearer as a loop
10result = []
11for item in get_items():
12 if not item.is_valid():
13 continue
14 if item.price >= threshold:
15 continue
16 result.append(transform(item))

If it does not fit on one readable line, write a loop. Fewer lines is not the goal. Readable code is.

Generators

A list comprehension builds the entire list in memory before you start using it. A generator expression looks the same but produces values one at a time, only when asked. Swap the square brackets for parentheses and you have a generator. No list is ever built.

Python
1# List comprehension: all 1M items created immediately
2squares_list = [x ** 2 for x in range(1_000_000)]
3
4# Generator expression: nothing computed yet
5squares_gen = (x ** 2 for x in range(1_000_000))
6
7import sys
8print(sys.getsizeof(squares_list)) # ~8 MB
9print(sys.getsizeof(squares_gen)) # 104 bytes

Consuming a generator

Generators are lazy, they only compute the next value when you ask for it. You can pass them directly to sum(), list(), max(), or any function that accepts an iterable. They can only be consumed once.

Python
1gen = (x ** 2 for x in range(5))
2
3# Iterate directly
4for v in gen:
5 print(v, end=" ") # 0 1 4 9 16
6print()
7
8# Useful with built-ins, no intermediate list needed
9total = sum(x ** 2 for x in range(1000))
10print(total) # 332833500
11
12biggest = max(len(word) for word in ["hello", "world", "python"])
13print(biggest) # 6
14
15# Generators are exhausted after one pass
16gen = (x for x in range(3))
17print(list(gen)) # [0, 1, 2]
18print(list(gen)) # [], exhausted

Generator functions with yield

A function with yield is a generator function. Calling it returns a generator object without running any code. Each next() call runs until the next yield, then pauses with its state intact.

Python
1def fibonacci():
2 a, b = 0, 1
3 while True:
4 yield a
5 a, b = b, a + b
6
7fib = fibonacci()
8print([next(fib) for _ in range(10)])
9# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
10
11# Reading a large file line by line, never loads the whole file
12def read_lines(path):
13 with open(path) as f:
14 for line in f:
15 yield line.rstrip()

Compare eager vs lazy evaluation below. In generator mode, each next() computes exactly one value.

Eager vs Lazy Explorer
squares = [x**2 for x in range(8)]

A list comprehension evaluates all items immediately. All 8 values are computed at once and stored in memory.

Loop patterns

Accumulate

Start with an identity value (0 for sum, 1 for product, [] for a list) and update it on every iteration. Built-ins like sum(), min(), and max() handle the common cases.

Python
1nums = [3, 1, 4, 1, 5, 9, 2, 6]
2
3# Use built-ins when you can
4total = sum(nums)
5biggest = max(nums)
6print(total, biggest) # 31 9
7
8# Manual accumulation, running product
9product = 1
10for n in nums:
11 product *= n
12print(product) # 6480
13
14# Collect into a structure
15evens = []
16for n in nums:
17 if n % 2 == 0:
18 evens.append(n)
19print(evens) # [4, 2, 6]

Find first

Return as soon as you find what you need. Continuing to iterate after finding the answer wastes time. Use next() with a generator for a one-liner version.

Python
1nums = [4, 7, 2, 9, 1, 5]
2
3# Loop version
4def first_odd(items):
5 for n in items:
6 if n % 2 != 0:
7 return n
8 return None
9
10print(first_odd(nums)) # 7
11
12# One-liner with next()
13first = next((n for n in nums if n % 2 != 0), None)
14print(first) # 7

Sliding window

Python
1def windows(seq, size):
2 for i in range(len(seq) - size + 1):
3 yield seq[i : i + size]
4
5data = [1, 2, 3, 4, 5]
6for w in windows(data, 3):
7 print(w) # [1,2,3] [2,3,4] [3,4,5]

Chunking

Python
1def chunks(lst, size):
2 for i in range(0, len(lst), size):
3 yield lst[i : i + size]
4
5data = list(range(10))
6for chunk in chunks(data, 3):
7 print(chunk)
8# [0,1,2] [3,4,5] [6,7,8] [9]

Pairwise iteration

Python
1from itertools import pairwise # Python 3.10+
2
3temps = [20, 23, 19, 25, 22]
4for prev, curr in pairwise(temps):
5 change = curr - prev
6 sign = "+" if change > 0 else ""
7 print(f"{prev} -> {curr} ({sign}{change})")