|$ curl https://forge-ai.dev/api/markdown?path=docs/python/loops
$cat docs/python-—-loops-&-control-flow.md
updated Recently·15 min read·published

Python — Loops & Control Flow

PythonBeginner
Introduction

Python has only one kind of for-loop: the for-each loop. There is no C-style for (i = 0; i < n; i++) — instead you iterate directly over elements of any iterable. Combined with while loops and the optional else clause, Python gives you readable control flow without sacrificing power.

The for Loop

The for loop iterates over any iterable — list, str, tuple, dict, set, file, generator. No index variable or condition check needed.

for_loop.py
Python
1# Iterating over a list
2fruits = ["apple", "banana", "cherry"]
3for fruit in fruits:
4 print(fruit)
5
6# Iterating over a string (character by character)
7for ch in "Python":
8 print(ch)
9
10# Iterating over a tuple
11for coord in (10, 20, 30):
12 print(coord)
13
14# Iterating over a dictionary (keys by default)
15user = {"name": "Alice", "age": 30, "role": "admin"}
16for key in user:
17 print(key) # name, age, role
18
19# Iterating over values
20for value in user.values():
21 print(value)
22
23# Iterating over key-value pairs
24for key, value in user.items():
25 print(f"{key}={value}")
26
27# Iterating over a set (order not guaranteed)
28for item in {3, 1, 4, 1, 5}:
29 print(item)
30
31# Iterating over a file (line by line)
32with open("data.txt") as f:
33 for line in f:
34 print(line.strip())

info

The loop variable persists after the loop ends. If the iterable is empty, the loop body never runs and no NameError occurs — the variable still binds to the last value (or is never created if empty).
range()

When you need a numeric index or a counted loop, use range(). It produces an immutable sequence of numbers without creating a list in memory.

range_demo.py
Python
1# range(stop) — 0 to stop-1
2for i in range(5):
3 print(i) # 0 1 2 3 4
4
5# range(start, stop)
6for i in range(2, 7):
7 print(i) # 2 3 4 5 6
8
9# range(start, stop, step)
10for i in range(0, 10, 2):
11 print(i) # 0 2 4 6 8
12
13# Negative step (count down)
14for i in range(10, 0, -1):
15 print(i) # 10 9 8 ... 1
16
17# Index-based iteration (when you need the index)
18fruits = ["apple", "banana", "cherry"]
19for i in range(len(fruits)):
20 print(i, fruits[i])
21
22# Prefer enumerate() instead (see next section)

info

In Python 2, range() returned a list and xrange() was the lazy version. In Python 3, range() is always lazy — it's a range object, not a list.
enumerate()

When you need both the index and the value, enumerate() is cleaner than range(len(...)). It yields (index, element) tuples.

enumerate_demo.py
Python
1fruits = ["apple", "banana", "cherry"]
2
3# Basic usage
4for i, fruit in enumerate(fruits):
5 print(f"{i}: {fruit}")
6# 0: apple
7# 1: banana
8# 2: cherry
9
10# Custom start index
11for i, fruit in enumerate(fruits, start=1):
12 print(f"{i}. {fruit}")
13# 1. apple
14# 2. banana
15# 3. cherry
16
17# enumerate() is lazy — returns an iterator
18e = enumerate(fruits)
19print(next(e)) # (0, 'apple')
20print(next(e)) # (1, 'banana')
zip()

zip() aggregates elements from multiple iterables into tuples, stopping at the shortest iterable.

zip_demo.py
Python
1names = ["Alice", "Bob", "Charlie"]
2scores = [95, 87, 91]
3
4# Parallel iteration
5for name, score in zip(names, scores):
6 print(f"{name}: {score}")
7
8# With enumerate() for index
9for i, (name, score) in enumerate(zip(names, scores), start=1):
10 print(f"{i}. {name}: {score}")
11
12# zip() with three or more iterables
13ids = [1, 2, 3]
14for uid, name, score in zip(ids, names, scores):
15 print(uid, name, score)
16
17# Unzipping — zip(*pairs)
18pairs = [("a", 1), ("b", 2), ("c", 3)]
19letters, numbers = zip(*pairs)
20print(letters) # ("a", "b", "c")
21print(numbers) # (1, 2, 3)
22
23# zip(strict=True) — Python 3.10+
24# Raises ValueError if lengths differ
25# a = [1, 2, 3]
26# b = [4, 5]
27# list(zip(a, b, strict=True)) # ValueError!
while Loops

Use while when you don't know the number of iterations in advance. It repeats as long as a condition is truthy.

while_demo.py
Python
1# Count-up loop
2i = 0
3while i < 5:
4 print(i)
5 i += 1
6
7# User-input loop (until sentinel)
8while True:
9 line = input("> ")
10 if line == "quit":
11 break
12 print(f"You said: {line}")
13
14# Retry loop with counter
15attempts = 0
16while attempts < 3:
17 password = input("Enter password: ")
18 if password == "secret":
19 print("Access granted")
20 break
21 attempts += 1
22else:
23 print("Account locked") # runs if loop completed without break
24
25# Infinite loop (be careful!)
26# while True:
27# pass # Ctrl+C to stop
break, continue & pass

break exits the loop entirely, continue skips to the next iteration, and pass is a no-op placeholder.

break_continue.py
Python
1# break — exit loop early
2for num in range(10):
3 if num == 5:
4 break
5 print(num) # 0 1 2 3 4
6
7# continue — skip current iteration
8for num in range(10):
9 if num % 2 == 0:
10 continue
11 print(num) # 1 3 5 7 9
12
13# pass — placeholder (does nothing)
14for i in range(5):
15 if i == 3:
16 pass # TODO: handle i==3 case
17 print(i)
18
19# break in nested loop (only breaks inner loop)
20for i in range(3):
21 for j in range(3):
22 if j == 1:
23 break
24 print(i, j)
25# 0 0
26# 1 0
27# 2 0
28
29# Using a flag to break outer loop
30found = False
31for i in range(5):
32 for j in range(5):
33 if i + j == 7:
34 found = True
35 break
36 if found:
37 break
38print(i, j) # 2 5
39
40# Alternative: for-else (see else-clause section)
else Clause on Loops

Python lets you attach an else block to both for and while loops. The else runs only if the loop completed normally — meaning it did not hit a break.

for_else.py
Python
1# for-else — search without a flag
2numbers = [1, 3, 5, 7, 9]
3for n in numbers:
4 if n % 2 == 0:
5 print("Found even number:", n)
6 break
7else:
8 print("No even numbers found") # runs because no break
9
10# while-else
11i = 0
12while i < 5:
13 print(i)
14 i += 1
15 if i == 3:
16 break
17else:
18 print("Completed all iterations") # skipped because break
19
20# Common idiom: prime check
21num = 17
22for i in range(2, int(num ** 0.5) + 1):
23 if num % i == 0:
24 print(f"{num} is divisible by {i}")
25 break
26else:
27 print(f"{num} is prime") # only if no divisor found

warning

The else on loops often confuses newcomers. Think of it as "no-break" — it runs when the loop wasn't interrupted by break. If the loop never enters the body (empty iterable), the else still runs.
Nested Loops

Nested loops are loops inside loops. Each inner loop runs completely for every iteration of the outer loop.

nested_loops.py
Python
1# Multiplication table
2for i in range(1, 4):
3 for j in range(1, 4):
4 print(f"{i} x {j} = {i * j}")
5 print() # blank line after each row
6
7# Matrix traversal
8matrix = [
9 [1, 2, 3],
10 [4, 5, 6],
11 [7, 8, 9],
12]
13for row in matrix:
14 for val in row:
15 print(val, end=" ")
16 print()
17
18# Flattening with nested comprehension
19flat = [val for row in matrix for val in row]
20print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
21
22# Nested dict iteration
23data = {
24 "Alice": {"age": 30, "city": "NYC"},
25 "Bob": {"age": 25, "city": "LA"},
26}
27for name, info in data.items():
28 print(f"{name}:")
29 for key, value in info.items():
30 print(f" {key}: {value}")
31
32# product() from itertools avoids deep nesting
33from itertools import product
34for i, j in product(range(3), range(3)):
35 print(i, j)
match — Pattern Matching (3.10+)

Python 3.10 introduced match/case — structural pattern matching. It's more powerful than a switch statement: it can match against literals, sequences, mappings, classes, and guard conditions.

match_demo.py
Python
1# Basic literal matching
2def respond(code: int) -> str:
3 match code:
4 case 200:
5 return "OK"
6 case 404:
7 return "Not Found"
8 case 500:
9 return "Server Error"
10 case _:
11 return "Unknown"
12
13# Matching against literals and OR patterns
14def classify(value):
15 match value:
16 case 0 | 1 | 2:
17 return "small"
18 case 3 | 4 | 5:
19 return "medium"
20 case _:
21 return "large"
22
23# Sequence pattern matching
24def process(item):
25 match item:
26 case [x, y]:
27 return f"Pair: {x}, {y}"
28 case [x, y, z]:
29 return f"Triple: {x}, {y}, {z}"
30 case [x, *rest]:
31 return f"First: {x}, rest: {rest}"
32 case _:
33 return "Not a list"
34
35# Mapping pattern matching
36def handle_request(req):
37 match req:
38 case {"method": "GET", "path": path}:
39 return f"GET {path}"
40 case {"method": "POST", "data": data}:
41 return f"POST {data}"
42 case _:
43 return "Unknown request"
44
45# Matching with guards
46def categorize(point):
47 match point:
48 case (x, y) if x == y:
49 return f"On diagonal ({x}, {y})"
50 case (x, y) if x > 0 and y > 0:
51 return f"Quadrant I ({x}, {y})"
52 case (x, y):
53 return f"Elsewhere ({x}, {y})"
54
55# Matching class instances
56from dataclasses import dataclass
57@dataclass
58class Point:
59 x: int
60 y: int
61
62def describe(pt):
63 match pt:
64 case Point(x=0, y=0):
65 return "Origin"
66 case Point(x=x, y=y):
67 return f"Point({x}, {y})"

info

match is not a statement-based replacement for if/elif chains — it shines when you need destructuring. For simple equality checks, an if/elif chain is still fine.
Best Practices

Python rewards idiomatic loop usage. Follow these conventions for readable and performant code.

best_practices.py
Python
1# ✅ DO: iterate directly over elements
2for fruit in fruits:
3 print(fruit)
4
5# ❌ DON'T: C-style index loop
6for i in range(len(fruits)):
7 print(fruits[i])
8
9# ✅ DO: use enumerate() when you need both index and value
10for i, fruit in enumerate(fruits):
11 print(i, fruit)
12
13# ❌ DON'T: manual index tracking
14i = 0
15for fruit in fruits:
16 print(i, fruit)
17 i += 1
18
19# ✅ DO: use zip() for parallel iteration
20for name, score in zip(names, scores):
21 print(name, score)
22
23# ❌ DON'T: index-based parallel access
24for i in range(len(names)):
25 print(names[i], scores[i])
26
27# ✅ DO: use for-else to avoid flag variables
28for n in numbers:
29 if condition(n):
30 break
31else:
32 handle_not_found()
33
34# ❌ DON'T: search flags
35found = False
36for n in numbers:
37 if condition(n):
38 found = True
39 break
40if not found:
41 handle_not_found()
42
43# ✅ DO: use _ for unused loop variables
44for _ in range(10):
45 print("Hello")
46
47# ✅ DO: keep loops short — extract loop bodies into functions
48for item in items:
49 process_item(item) # vs 20 lines inline
50
51# ✅ DO: prefer itertools for complex iteration patterns
52from itertools import chain, cycle, accumulate
53for val in chain(list1, list2):
54 print(val)

warning

Never mutate a list (insert/remove) while iterating over it. Create a copy instead: for item in items[:]: or use a list comprehension.
Related Topics
$Blueprint — Engineering Documentation·Section ID: PYTHON-LOOPS·Revision: 1.0