Python — Loops & Control Flow
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 iterates over any iterable — list, str, tuple, dict, set, file, generator. No index variable or condition check needed.
| 1 | # Iterating over a list |
| 2 | fruits = ["apple", "banana", "cherry"] |
| 3 | for fruit in fruits: |
| 4 | print(fruit) |
| 5 | |
| 6 | # Iterating over a string (character by character) |
| 7 | for ch in "Python": |
| 8 | print(ch) |
| 9 | |
| 10 | # Iterating over a tuple |
| 11 | for coord in (10, 20, 30): |
| 12 | print(coord) |
| 13 | |
| 14 | # Iterating over a dictionary (keys by default) |
| 15 | user = {"name": "Alice", "age": 30, "role": "admin"} |
| 16 | for key in user: |
| 17 | print(key) # name, age, role |
| 18 | |
| 19 | # Iterating over values |
| 20 | for value in user.values(): |
| 21 | print(value) |
| 22 | |
| 23 | # Iterating over key-value pairs |
| 24 | for key, value in user.items(): |
| 25 | print(f"{key}={value}") |
| 26 | |
| 27 | # Iterating over a set (order not guaranteed) |
| 28 | for item in {3, 1, 4, 1, 5}: |
| 29 | print(item) |
| 30 | |
| 31 | # Iterating over a file (line by line) |
| 32 | with open("data.txt") as f: |
| 33 | for line in f: |
| 34 | print(line.strip()) |
info
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.
| 1 | # range(stop) — 0 to stop-1 |
| 2 | for i in range(5): |
| 3 | print(i) # 0 1 2 3 4 |
| 4 | |
| 5 | # range(start, stop) |
| 6 | for i in range(2, 7): |
| 7 | print(i) # 2 3 4 5 6 |
| 8 | |
| 9 | # range(start, stop, step) |
| 10 | for i in range(0, 10, 2): |
| 11 | print(i) # 0 2 4 6 8 |
| 12 | |
| 13 | # Negative step (count down) |
| 14 | for i in range(10, 0, -1): |
| 15 | print(i) # 10 9 8 ... 1 |
| 16 | |
| 17 | # Index-based iteration (when you need the index) |
| 18 | fruits = ["apple", "banana", "cherry"] |
| 19 | for i in range(len(fruits)): |
| 20 | print(i, fruits[i]) |
| 21 | |
| 22 | # Prefer enumerate() instead (see next section) |
info
When you need both the index and the value, enumerate() is cleaner than range(len(...)). It yields (index, element) tuples.
| 1 | fruits = ["apple", "banana", "cherry"] |
| 2 | |
| 3 | # Basic usage |
| 4 | for i, fruit in enumerate(fruits): |
| 5 | print(f"{i}: {fruit}") |
| 6 | # 0: apple |
| 7 | # 1: banana |
| 8 | # 2: cherry |
| 9 | |
| 10 | # Custom start index |
| 11 | for 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 |
| 18 | e = enumerate(fruits) |
| 19 | print(next(e)) # (0, 'apple') |
| 20 | print(next(e)) # (1, 'banana') |
zip() aggregates elements from multiple iterables into tuples, stopping at the shortest iterable.
| 1 | names = ["Alice", "Bob", "Charlie"] |
| 2 | scores = [95, 87, 91] |
| 3 | |
| 4 | # Parallel iteration |
| 5 | for name, score in zip(names, scores): |
| 6 | print(f"{name}: {score}") |
| 7 | |
| 8 | # With enumerate() for index |
| 9 | for i, (name, score) in enumerate(zip(names, scores), start=1): |
| 10 | print(f"{i}. {name}: {score}") |
| 11 | |
| 12 | # zip() with three or more iterables |
| 13 | ids = [1, 2, 3] |
| 14 | for uid, name, score in zip(ids, names, scores): |
| 15 | print(uid, name, score) |
| 16 | |
| 17 | # Unzipping — zip(*pairs) |
| 18 | pairs = [("a", 1), ("b", 2), ("c", 3)] |
| 19 | letters, numbers = zip(*pairs) |
| 20 | print(letters) # ("a", "b", "c") |
| 21 | print(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! |
Use while when you don't know the number of iterations in advance. It repeats as long as a condition is truthy.
| 1 | # Count-up loop |
| 2 | i = 0 |
| 3 | while i < 5: |
| 4 | print(i) |
| 5 | i += 1 |
| 6 | |
| 7 | # User-input loop (until sentinel) |
| 8 | while True: |
| 9 | line = input("> ") |
| 10 | if line == "quit": |
| 11 | break |
| 12 | print(f"You said: {line}") |
| 13 | |
| 14 | # Retry loop with counter |
| 15 | attempts = 0 |
| 16 | while attempts < 3: |
| 17 | password = input("Enter password: ") |
| 18 | if password == "secret": |
| 19 | print("Access granted") |
| 20 | break |
| 21 | attempts += 1 |
| 22 | else: |
| 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 exits the loop entirely, continue skips to the next iteration, and pass is a no-op placeholder.
| 1 | # break — exit loop early |
| 2 | for num in range(10): |
| 3 | if num == 5: |
| 4 | break |
| 5 | print(num) # 0 1 2 3 4 |
| 6 | |
| 7 | # continue — skip current iteration |
| 8 | for 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) |
| 14 | for 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) |
| 20 | for 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 |
| 30 | found = False |
| 31 | for 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 |
| 38 | print(i, j) # 2 5 |
| 39 | |
| 40 | # Alternative: for-else (see else-clause section) |
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.
| 1 | # for-else — search without a flag |
| 2 | numbers = [1, 3, 5, 7, 9] |
| 3 | for n in numbers: |
| 4 | if n % 2 == 0: |
| 5 | print("Found even number:", n) |
| 6 | break |
| 7 | else: |
| 8 | print("No even numbers found") # runs because no break |
| 9 | |
| 10 | # while-else |
| 11 | i = 0 |
| 12 | while i < 5: |
| 13 | print(i) |
| 14 | i += 1 |
| 15 | if i == 3: |
| 16 | break |
| 17 | else: |
| 18 | print("Completed all iterations") # skipped because break |
| 19 | |
| 20 | # Common idiom: prime check |
| 21 | num = 17 |
| 22 | for i in range(2, int(num ** 0.5) + 1): |
| 23 | if num % i == 0: |
| 24 | print(f"{num} is divisible by {i}") |
| 25 | break |
| 26 | else: |
| 27 | print(f"{num} is prime") # only if no divisor found |
warning
Nested loops are loops inside loops. Each inner loop runs completely for every iteration of the outer loop.
| 1 | # Multiplication table |
| 2 | for 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 |
| 8 | matrix = [ |
| 9 | [1, 2, 3], |
| 10 | [4, 5, 6], |
| 11 | [7, 8, 9], |
| 12 | ] |
| 13 | for row in matrix: |
| 14 | for val in row: |
| 15 | print(val, end=" ") |
| 16 | print() |
| 17 | |
| 18 | # Flattening with nested comprehension |
| 19 | flat = [val for row in matrix for val in row] |
| 20 | print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9] |
| 21 | |
| 22 | # Nested dict iteration |
| 23 | data = { |
| 24 | "Alice": {"age": 30, "city": "NYC"}, |
| 25 | "Bob": {"age": 25, "city": "LA"}, |
| 26 | } |
| 27 | for 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 |
| 33 | from itertools import product |
| 34 | for i, j in product(range(3), range(3)): |
| 35 | print(i, j) |
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.
| 1 | # Basic literal matching |
| 2 | def 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 |
| 14 | def 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 |
| 24 | def 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 |
| 36 | def 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 |
| 46 | def 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 |
| 56 | from dataclasses import dataclass |
| 57 | @dataclass |
| 58 | class Point: |
| 59 | x: int |
| 60 | y: int |
| 61 | |
| 62 | def 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
Python rewards idiomatic loop usage. Follow these conventions for readable and performant code.
| 1 | # ✅ DO: iterate directly over elements |
| 2 | for fruit in fruits: |
| 3 | print(fruit) |
| 4 | |
| 5 | # ❌ DON'T: C-style index loop |
| 6 | for i in range(len(fruits)): |
| 7 | print(fruits[i]) |
| 8 | |
| 9 | # ✅ DO: use enumerate() when you need both index and value |
| 10 | for i, fruit in enumerate(fruits): |
| 11 | print(i, fruit) |
| 12 | |
| 13 | # ❌ DON'T: manual index tracking |
| 14 | i = 0 |
| 15 | for fruit in fruits: |
| 16 | print(i, fruit) |
| 17 | i += 1 |
| 18 | |
| 19 | # ✅ DO: use zip() for parallel iteration |
| 20 | for name, score in zip(names, scores): |
| 21 | print(name, score) |
| 22 | |
| 23 | # ❌ DON'T: index-based parallel access |
| 24 | for i in range(len(names)): |
| 25 | print(names[i], scores[i]) |
| 26 | |
| 27 | # ✅ DO: use for-else to avoid flag variables |
| 28 | for n in numbers: |
| 29 | if condition(n): |
| 30 | break |
| 31 | else: |
| 32 | handle_not_found() |
| 33 | |
| 34 | # ❌ DON'T: search flags |
| 35 | found = False |
| 36 | for n in numbers: |
| 37 | if condition(n): |
| 38 | found = True |
| 39 | break |
| 40 | if not found: |
| 41 | handle_not_found() |
| 42 | |
| 43 | # ✅ DO: use _ for unused loop variables |
| 44 | for _ in range(10): |
| 45 | print("Hello") |
| 46 | |
| 47 | # ✅ DO: keep loops short — extract loop bodies into functions |
| 48 | for item in items: |
| 49 | process_item(item) # vs 20 lines inline |
| 50 | |
| 51 | # ✅ DO: prefer itertools for complex iteration patterns |
| 52 | from itertools import chain, cycle, accumulate |
| 53 | for val in chain(list1, list2): |
| 54 | print(val) |
warning