Loops
Python provides two loop forms: for and while. The for is the conventional iteration form — it iterates over any iterable (a list, tuple, dict, set, file object, generator, or any object with __iter__). The while is for condition-driven iteration. Python does not have a C-style for (init; cond; step) loop; the iteration is always over an iterable. Comprehensions — list, set, dict, and generator — admit compact construction of new collections; they are the conventional Python alternative to explicit loops for filter-and-map operations. The else clause on for and while (a Python peculiarity) admits “this runs if the loop completes without break”.
for
The for loop iterates over any iterable:
for item in iterable:
process(item)
Common iteration sources:
# List:
for x in [1, 2, 3, 4]:
print(x)
# Range:
for i in range(10):
print(i) # 0, 1, ..., 9
for i in range(1, 11):
print(i) # 1, 2, ..., 10
for i in range(0, 100, 5):
print(i) # 0, 5, 10, ..., 95
# Dict (iterates keys by default):
for key in {"a": 1, "b": 2}:
print(key)
for key, value in {"a": 1, "b": 2}.items():
print(f"{key}: {value}")
# Tuple unpacking:
for x, y in [(1, 2), (3, 4), (5, 6)]:
print(f"{x}, {y}")
# String:
for ch in "hello":
print(ch)
# File:
with open("input.txt") as f:
for line in f:
process(line.rstrip())
# Generator:
for n in (x ** 2 for x in range(10)):
print(n)
The for loop calls iter() on the iterable to obtain an iterator, then calls next() until StopIteration is raised. The mechanism is the duck-typed iteration protocol; any object with __iter__ is iterable.
enumerate for index-and-value
To iterate with both the index and the value:
for i, item in enumerate(items):
print(f"{i}: {item}")
# Starting from 1:
for i, item in enumerate(items, start=1):
print(f"{i}: {item}")
The enumerate is the conventional Python idiom for “iterate with the position”.
zip for parallel iteration
for a, b in zip(list_a, list_b):
print(f"{a}, {b}")
# With multiple iterables:
for a, b, c in zip(list_a, list_b, list_c):
print(a, b, c)
# Stop at the shortest (default):
for x, y in zip([1, 2, 3], [10, 20]):
print(x, y) # (1, 10), (2, 20); 3 is dropped
# Strict mode (3.10+): error on length mismatch
for x, y in zip([1, 2, 3], [10, 20], strict=True):
pass # raises ValueError
zip produces an iterator of tuples; it stops at the shortest iterable. The strict=True (3.10+) admits checking that all iterables have the same length.
For zip_longest — fill the shorter iterables with a default — itertools.zip_longest.
reversed and sorted
Common iteration transformations:
for item in reversed(items): # iterate in reverse
print(item)
for item in sorted(items): # iterate sorted
print(item)
for item in sorted(items, reverse=True):
print(item)
for item in sorted(items, key=lambda x: x.priority):
print(item)
Both produce a new iterator/list; the original is unchanged.
while
The while loop tests a condition before each iteration:
while not queue.empty():
item = queue.dequeue()
process(item)
The conventional Python uses:
- Polling loops where iteration count isn’t known.
- Read-loops where each iteration consumes the next input.
- State-machine-style loops.
Python does not have do … while; the conventional substitute is while True: with an explicit break:
while True:
line = input("> ")
if line == "quit":
break
process(line)
Or, with the walrus:
while (line := input("> ")) != "quit":
process(line)
break, continue, pass
Three loop-control statements:
break— exit the innermost enclosingfororwhile.continue— skip the rest of the current iteration; proceed to the next.pass— no-op (rarely needed in loops; useful for empty bodies during development).
for item in items:
if item.is_invalid():
continue
if item.matches(target):
found = item
break
process(item)
Python does not have a labelled break or continue; for nested loops, the conventional substitute is a flag, a goto-substitute via a function with return, or refactoring:
def find_in_matrix(target, matrix):
for i, row in enumerate(matrix):
for j, value in enumerate(row):
if value == target:
return i, j
return None
The function-based form is the conventional Python idiom for “exit nested loops on a condition”.
for/while else
A peculiarity of Python: for and while admit an else clause that runs if the loop completes without break:
for item in items:
if item.matches(target):
print(f"found: {item}")
break
else:
print("not found")
# Equivalent to:
found = False
for item in items:
if item.matches(target):
print(f"found: {item}")
found = True
break
if not found:
print("not found")
The else clause is occasionally useful for search loops; the conventional alternative is the function-based form (with return for the found case and a fall-through for “not found”).
The construct is broadly considered confusing — many Python programmers don’t know it exists or misremember its semantics. Use sparingly.
Comprehensions
Python’s comprehensions admit compact construction of lists, sets, dicts, and generators:
List comprehension
squares = [x ** 2 for x in range(10)]
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
evens = [x for x in range(20) if x % 2 == 0]
# [0, 2, 4, ..., 18]
pairs = [(x, y) for x in range(3) for y in range(3) if x != y]
# nested loops; rightmost is innermost
flattened = [item for sublist in lists for item in sublist]
The form: [expr for var in iterable [if cond] [for ... [if ...]]]. The expression is computed for each combination of values that satisfy all the conditions.
The conventional uses:
- Map:
[f(x) for x in xs] - Filter:
[x for x in xs if pred(x)] - Map and filter:
[f(x) for x in xs if pred(x)]
Set and dict comprehensions
unique_lengths = {len(s) for s in strings}
char_counts = {ch: text.count(ch) for ch in set(text)}
The set form uses {}; the dict form uses {key: value for ...}. Both follow the same syntax pattern as list comprehensions.
Generator expression
sum_of_squares = sum(x ** 2 for x in range(1000))
# generator: doesn't materialise the list
The generator form uses (). It produces values lazily; substantial savings when the consumer doesn’t need the full collection.
For consumption by a function that accepts an iterable, the parentheses around a generator may be omitted when the generator is the single argument:
sum(x ** 2 for x in range(1000)) # OK; the parens are the function call's
max(x for x in items if x > 0)
When to use comprehensions
The conventional discipline:
- Use comprehensions for filter, map, and small transformations.
- Use explicit loops for substantial bodies (multiple statements per iteration).
- Use generator expressions when the result is consumed lazily and need not be materialised.
# Comprehension — concise, readable:
positives = [x for x in numbers if x > 0]
# Explicit loop — preferable for non-trivial body:
result = []
for x in numbers:
if x > 0:
cleaned = clean(x)
validated = validate(cleaned)
result.append(validated)
The transition point is roughly “the body is more than one expression”; beyond that, the explicit loop is conventionally clearer.
for/while patterns
Counting
count = 0
for item in items:
if item.is_active():
count += 1
Or:
count = sum(1 for item in items if item.is_active())
Iterating with index
for i, item in enumerate(items):
print(f"{i}: {item}")
Iterating two collections in parallel
for a, b in zip(list_a, list_b):
process(a, b)
Reading lines from a file
with open(path) as f:
for line in f:
process(line.rstrip())
Each line includes its newline; rstrip() removes it. The for line in f form iterates lazily — the entire file is not loaded.
Removing during iteration
# WRONG: modifying the list during iteration
for item in items:
if item.is_expired():
items.remove(item) # produces unpredictable results
# Right: iterate a copy or use a comprehension
items[:] = [item for item in items if not item.is_expired()]
The items[:] = ... form modifies the list in place; the comprehension produces a new list.
For mutable mappings:
# Build a list of keys to delete first:
to_delete = [key for key, value in d.items() if value is None]
for key in to_delete:
del d[key]
Iterating until a condition
# while True with break:
while True:
item = source.next()
if item is None:
break
process(item)
# Or with iter and a sentinel (the conventional Python idiom):
for item in iter(source.next, None):
process(item)
The two-argument iter(callable, sentinel) admits “call the function repeatedly until it returns the sentinel”. The form is rare but useful for certain stream-reading patterns.
Infinite loop
while True:
do_work()
The conventional Python infinite loop (no for(;;) form).
Generator-based iteration
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
for n in fibonacci():
if n > 1000:
break
print(n)
The generator produces values lazily; the consumer takes only what it needs. The full treatment is in Iterators and generators.
A note on the absence of C-style for
Python’s for is not the C for(init; cond; step) form. The substitutes:
- Index iteration:
for i in range(n): - Index and value:
for i, x in enumerate(items): - Custom step:
for i in range(start, stop, step): - State machines and complex conditions:
whilewith explicit state.
The conventional Python style favours iteration over a collection over manual index manipulation. The for x in collection form makes the iteration explicit; the underlying iterator handles the bookkeeping.