Polyglot
Languages Python loops
Python § loops

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 enclosing for or while.
  • 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: while with 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.