# Control flow

Source: https://codewiki.com/python/control-flow/

> - **what**: Control flow determines which code runs next. Python maps data and program state to execution paths through conditions, loops, jump statements, and `match`.
> - **trap**: Conditions test an object's truth value, while Boolean operators return operands; missing progress in a `while` loop or mutating a container during iteration also changes paths silently.
> - **fix**: State branch priority and termination conditions explicitly, iterate directly, and test generated code with boundaries, empty inputs, and every exit path.

## What it is and why it exists

Control flow is the set of rules by which a program chooses its next statement. Without it, statements could only run once from top to bottom. Conditions, loops, and jumps let the same program choose paths for different inputs, repeat work, or finish early.

Python's core tools have distinct responsibilities. `if`, `elif`, and `else` choose a conditional branch, `for` consumes an iterable, `while` repeats while a condition remains true, and `break`, `continue`, and `return` alter the normal progress of the current structure. `match` selects a branch from a value's shape and contents rather than comparing only one scalar.

You meet these structures in input validation, collection processing, searches, retries, state machines, and request routing. Memorizing syntax is less important than answering three questions: in what order are conditions evaluated, how does a loop obtain its next item, and which paths leave the current branch, loop, or function?

Control flow still follows Python's block structure. The indented suite after a colon belongs to its statement; changing indentation changes semantics, not merely presentation. Keeping each branch and loop body short makes its execution paths easier to verify.

## How it works

Python starts at the current statement and chooses a successor according to a condition or iteration state. Every condition expression undergoes truth value testing; its result need not literally be `True` or `False`.

Choose a structure by identifying what drives the path and how much scope must be exited:

- Use a conditional expression for one value choice and an `if` chain for mutually exclusive suites.
- Use `for` when an iterable input already exists instead of managing an unnecessary index.
- Use `while` when repetition depends on changing state, and state the termination bound.
- Consider `match` when a branch depends on both the shape and contents of nested data.
- Use `return` to leave an entire function rather than treating `break` as a multilevel jump.

### A conditional chain chooses one path

`if` evaluates its condition first. If the result is true, Python executes that suite and skips the remaining branches in the same chain. Otherwise, it checks each `elif` in order and may finally enter `else`. Condition order is therefore part of the business priority.

Put narrower conditions before broader ones. If you check `score >= 60` first, a later `score >= 90` can never run. Mutually exclusive conditions fit one `if` chain; independent conditions that may all hold belong in separate `if` statements.

The conditional expression `value_if_true if condition else value_if_false` tests `condition` and evaluates only one of its result expressions. It suits a simple value choice, not several side effects or nested decisions.

### `for` consumes an iterator

An iterable can provide an iterator, which yields values one at a time until it signals completion. `for` handles obtaining that iterator, asking for the next item, and recognizing completion. It is usually more direct than manual indexing or a `while` loop when processing a collection.

`range(stop)` represents integers from `0` up to but excluding `stop`; it does not build a list of them first. `enumerate(iterable, start=1)` yields a counter with each value, while `dict.items()` yields key-value pairs. These tools make a loop state the data it needs instead of retrieving it indirectly through indices.

A `for` target can unpack each item, as in `for key, value in mapping.items()`. Every item must have a compatible shape or the loop raises `ValueError` at that item. After normal completion, the target name remains bound to the final item; it is not a block-scoped variable that disappears with the loop.

### `while` depends on a progress invariant

`while` tests its condition before each iteration. A true result executes the body and returns to the top for another test; an initially false result means the body never runs. This fits work whose count depends on changing state, such as bounded retries or reading until a sentinel appears.

A reliable `while` loop has a progress invariant: every path that continues moves state closer to termination. Incrementing a counter, shrinking a queue, or transitioning state can establish progress. If a `continue` bypasses the update, the loop can remain on the same input forever.

### Jump statements affect different scopes

Several short statements look similar but have different reach:

- `break` immediately terminates the innermost `for` or `while` loop.
- `continue` skips the rest of this iteration and starts the innermost loop's next one.
- `return` terminates the whole function and gives an optional result back to its caller.
- `pass` does nothing; it is an empty statement where grammar requires a statement.

`pass` does not skip later code as `continue` does. After `pass` executes in a loop, the next statement in that iteration still runs. It can temporarily mark an unimplemented body, but an empty branch in published code should usually explain why that case is intentionally ignored.

A `break` in nested loops leaves only the innermost loop. When a successful search should leave several nested structures, extracting it into a function and using `return` is usually clearer than maintaining several flag variables.

### Nested control and guard clauses

Every nesting level adds another condition the reader must hold in mind. A guard clause handles invalid input or an already-complete case with an early `return` or `continue`, leaving the main path at shallower indentation.

A guard clause is not merely a mechanically inverted condition. Early exit must preserve side effects and cleanup order, and it must not turn “skip this item” into “end the function.” Compare the rewritten paths one by one.

Several independent `if` statements are not equivalent to an `if`, `elif` chain. The former can execute several suites, while the latter selects at most one. Models that interchange them automatically to “reduce nesting” often change business behavior.

### Function exits and exception paths

Once `return` executes, later ordinary statements in the function do not run. If a loop only searches for a result, returning directly from the successful path and returning a not-found result after the loop is often easier for a team to read than a flag plus loop `else`.

`raise` and an unhandled exception also transfer control, with the exception-handling structure determining the destination. Cleanup in `finally` runs during normal exit, `return`, `break`, or exception propagation. Do not add a new `return` in `finally` that hides the original result or exception.

A control-flow review must include failure paths. Reading only the successful branch misses whether resources are cleaned after an exception, whether a loop retries unexpectedly, and whether callers can distinguish “not found” from “processing failed.”

### Loop `else` means no `break` ran

Python's loop `else` runs when a `for` ends because its iterator is exhausted or a `while` ends because its condition becomes false. It does not run if that same loop executes `break`. A `continue` does not suppress it because `continue` ends only the current iteration.

This syntax is especially useful for searches: break when the target is found, and let `else` handle exhausting the input without a match. Reading it as “run if no `break` occurred” is usually more accurate than “run when the loop condition is false,” which does not explain `for` and obscures early exit.

### `match` selects by shape and value

Structural pattern matching evaluates the subject after `match` once and tries cases from top to bottom. The first branch whose pattern matches and whose guard is true runs, and the rest are skipped. If no case matches, the whole statement does nothing, so `case _` commonly makes the default path explicit.

A pattern is not an ordinary Boolean expression. Sequence patterns can check length and unpack elements, mapping patterns can require keys, and class patterns can check types and extract attributes. A capture name in a pattern binds a value; it does not compare against an existing local variable.

A pattern may have a match guard, written `case pattern if condition`. Python evaluates it only after the pattern succeeds. Guards express constraints that data shape alone cannot, but side effects make matching hard to reason about, so keep them as simple predicates.

## Examples

These four examples add branches, loop exits, and structural matching in stages. Each program runs independently, and its output comes from a local execution.

### Classify pending orders

The first example iterates over orders directly and uses `enumerate()` for reader-facing positions. `continue` keeps unpaid orders out of shipment classification. Each paid order enters exactly one amount branch, ordered from highest threshold to lowest.

<!-- quick -->

```python
# file: route_orders.py
orders = [
    {"id": "A-104", "paid": True, "total": 135},
    {"id": "B-205", "paid": False, "total": 80},
    {"id": "C-309", "paid": True, "total": 24},
    {"id": "D-410", "paid": True, "total": 72},
]

for position, order in enumerate(orders, start=1):
    if not order["paid"]:
        print(f'{position}. {order["id"]}: hold')
        continue

    if order["total"] >= 100:
        lane = "priority"
    elif order["total"] >= 50:
        lane = "standard"
    else:
        lane = "economy"

    print(f'{position}. {order["id"]}: {lane}')
```

```text
1. A-104: priority
2. B-205: hold
3. C-309: economy
4. D-410: standard
```

<!-- /quick -->

The conditions descend from the highest amount, so each paid order receives one classification. Putting `total >= 50` first would incorrectly absorb some priority orders, demonstrating why branch order itself needs tests.

### `else` after a search

Here `else` aligns with `for`, not `if`. Finding the product executes `break`, which also skips `else`. The loop reaches `else` only after searching every shelf without a match.

```python
# file: find_product.py
inventory = [
    ["cable", "mouse"],
    ["keyboard", "stand"],
    ["camera", "microphone"],
]


def locate(product):
    for shelf, products in enumerate(inventory, start=1):
        if product in products:
            print(f"{product}: shelf {shelf}")
            break
    else:
        print(f"{product}: unavailable")


locate("camera")
locate("adapter")
```

```text
camera: shelf 3
adapter: unavailable
```

The first call breaks at the third shelf, while the second exhausts the iterator. The two outputs cover the loop's early-exit and normal-completion paths.

### Poll state with a bound

The number of polls depends on state, so `while` is more natural than iterating over a business collection. The counter advances before any possible `continue` or `break`, making every iteration progress toward the retry limit.

```python
# file: poll_job.py
poll_results = iter(["pending", "pending", "ready"])
max_attempts = 4
attempt = 0

while attempt < max_attempts:
    attempt += 1
    result = next(poll_results, "unavailable")
    print(f"attempt {attempt}: {result}")

    if result == "ready":
        print("job completed")
        break
else:
    print("job did not complete")
```

```text
attempt 1: pending
attempt 2: pending
attempt 3: ready
job completed
```

The third attempt gets `ready` and breaks, so the failure message does not appear. If all four attempts missed `ready`, the condition would eventually become false and `else` would report non-completion.

### Route by event shape

The final example places dictionary keys, sequence shape, a type check, and a guard into cases. The more specific nonnegative-coordinate branch must precede the general move branch, or the latter would match every two-element point first.

```python
# file: route_events.py
def describe_event(event):
    match event:
        case {"kind": "move", "point": [x, y]} if x >= 0 and y >= 0:
            return f"move to ({x}, {y})"
        case {"kind": "move", "point": [x, y]}:
            return f"move outside grid: ({x}, {y})"
        case {"kind": "message", "text": str(text)}:
            return f"message: {text}"
        case _:
            return "unsupported event"


events = [
    {"kind": "move", "point": [3, 7]},
    {"kind": "move", "point": [-1, 4]},
    {"kind": "message", "text": "deploy"},
    {"kind": "message", "text": 404},
]

for event in events:
    print(describe_event(event))
```

```text
move to (3, 7)
move outside grid: (-1, 4)
message: deploy
unsupported event
```

`str(text)` is a class pattern that requires the corresponding value to be a string. A plain `text` capture pattern would accept the integer `404`, so the default branch rejects the final event whose shape is right but type is wrong.

## Pitfalls

### Combining conditions like natural language

> **Pitfall:** `if status == "ready" or "queued"` always passes because the nonempty string `"queued"` is truthy by itself.

**Fix:** write each complete comparison, such as `status == "ready" or status == "queued"`. Prefer `status in {"ready", "queued"}` for membership in several allowed values. Test a value outside the set so positive examples alone cannot hide the error.

The same problem appears with ranges. Python supports `0 <= percentage <= 100`; do not write `percentage >= 0 or percentage <= 100`, because almost every number satisfies at least one side.

### Mutating the list being iterated

> **Pitfall:** Removing elements from `items` inside `for item in items` shifts later positions, so the iterator can skip adjacent items.

**Fix:** build a new list when filtering. If in-place deletion is truly required, iterate over `items.copy()` or choose reverse indexing when the problem warrants it. Do not assume every container has the same mutation rules during iteration.

Changing the size of a dictionary or set during iteration commonly raises `RuntimeError`, while a list may keep running and return the wrong result. Generated code that equates “no exception” with safety is especially likely to miss this silent failure.

### A `while` path makes no progress

> **Pitfall:** When a state update sits at the bottom of the body, an earlier `continue` can bypass it and cause an infinite loop.

**Fix:** advance a counter near the top or ensure that every path back to the condition performs the update. State a progress invariant and test it with inputs that trigger every `continue`. External polling also needs a clear attempt limit or deadline.

`while True` is not inherently wrong, but its exit must be visible and reachable. When a loop depends on a queue, network, or user input, “it will eventually return” is not a verifiable termination strategy.

### Treating loop `else` as the `if` alternative

> **Pitfall:** Loop `else` depends on whether `break` ran, not on the result of the final `if` inside the loop body.

**Fix:** read it as “run if not found,” and verify that the success path actually executes `break`. If the flow remains opaque, extract the search into a result-returning function and let the caller handle that result with an ordinary `if`.

`continue` does not skip loop `else`, because it starts only the next iteration. A `return` or unhandled exception leaves the current function or suite directly, so execution naturally does not proceed to the loop's attached `else`.

### Mistaking a bare name for a constant in `match`

> **Pitfall:** A bare name in `case RED:` is normally a capture pattern: it matches any subject and binds that subject to `RED` instead of comparing with an existing variable.

**Fix:** put string, number, and enum literals directly in the pattern. Use a dotted name for a named constant, such as `case Color.RED:`. Order specific branches before general ones and always account for unrecognized input.

A catch-all capture makes later cases unreachable, and Python usually rejects obvious instances at compile time. Guards and complex patterns can still obscure ordering mistakes, so prepare one input for each branch and one unknown input.

<!-- deep -->

## Iteration boundaries and state ownership

An iterable and an iterator are not the same concept. A list can normally create a fresh iterator for each `for`, whereas an iterator stores a position and usually moves only forward. Looping over the same exhausted iterator again does not restart it.

Before each iteration, `for` requests the next item from the iterator and executes the body only after obtaining a value. When the iterator reports exhaustion, the loop completes normally and may enter `else`. The loop protocol handles that completion signal; ordinary loop bodies should not catch it manually.

Executing `break` does not reset the underlying iterator. Other code holding that iterator can continue from the remaining position. This supports staged parsing, but it can also make callers share progress accidentally.

Lists, tuples, and `range` can be iterated repeatedly, but generator expressions and generator objects carry one consumption state. A function that scans input more than once should declare that it needs a reiterable collection or deliberately materialize data once, not assume every `iterable` can replay.

A loop variable remains after the loop, but empty iteration never assigns it. Reading a name that can only be bound in the body raises `NameError` or `UnboundLocalError` on empty input. Initialize a result to an explicit sentinel or return directly from the found path to cover that boundary.

`zip()` stops by default when its shortest input is exhausted, silently ignoring the tail of longer inputs. When equal lengths are required, Python 3.10 and later provide `zip(..., strict=True)`, which raises `ValueError` on a length mismatch instead of dropping data.

Iteration state also has ownership. Giving one iterator to two consumers makes them take items in an interleaved order. If each consumer should see the full input, create separate iterators or pass an iterable that can create them.

## Truth values and short-circuit evaluation

By default, an object is true unless it defines itself as false. Built-in false values include `None`, `False`, numeric zero, and empty strings and containers. A user-defined object can return a Boolean from `__bool__()`; without that method, a zero result from `__len__()` also makes the object false.

Consequently, `if items` clearly means “the collection is nonempty,” but it does not always preserve domain meaning. If `None` means “not supplied” and an empty list means “supplied with no members,” use `if value is None` to retain that distinction. Generated code often collapses both states into one false path.

`and` and `or` truth-test their left operand and may skip the right operand. `left and right` returns `left` when it is false and otherwise returns `right`; `left or right` returns `left` when it is true and otherwise returns `right`. They return one operand, not necessarily a Boolean.

Short-circuiting can safely guard a later access: `user is not None and user.active` never reads the attribute when `user` is `None`. Do not hide side-effecting calls in the right operand, because whether they execute then depends on left-side data and the control path becomes easy to miss.

### Custom truth should be cheap and stable

`__bool__()` must return an actual `bool`; another result type raises `TypeError`. `__len__()` should likewise not make a network request or consume an iterator merely to answer whether an object is empty, because conditions may inspect an object more often than its caller expects.

An iterator object itself is normally true even after it is exhausted. To determine whether data remains, request the next item, use a sentinel, or choose a data structure with an explicit length. `if iterator` is not an exhaustion check.

Chained comparisons evaluate the middle expression only once. `lower <= value < upper` states both bounds without computing `value` twice; it carries the same intent as connecting two comparisons with `and`, but maps more directly to a mathematical interval.

## Precise structural matching semantics

`match` evaluates its subject once and then tries each `case` in source order. Only after a pattern succeeds does Python evaluate its guard; a false guard moves matching to the next case. When a case body finishes, control leaves the entire `match`; there is no automatic fallthrough like that of some languages' `switch` statements.

Sequence patterns inspect element structure, but do not split `str`, `bytes`, or `bytearray` as general sequences. Mapping patterns require the keys they spell and allow extra keys by default. When extra contents matter, capture them with `**rest` and validate them in a guard.

Class patterns depend on the type and its pattern-matching protocol. Positional patterns follow the class's `__match_args__`, whereas keyword patterns read named attributes. Keyword form is usually clearer for an unfamiliar class and does not assume an unverified positional order.

An OR pattern is written `pattern_a | pattern_b`; both sides must bind the same set of names so the body receives a consistent local environment. `_` is a wildcard and creates no binding. Other bare names normally capture the subject.

### Case order and guard side effects

The interpreter chooses the first branch whose pattern succeeds and whose guard is true. Specific patterns therefore need to precede general patterns that cover them. The compiler rejects some obviously unreachable cases, but it cannot decide whether your business priorities are in the right order.

A guard may call a function, but a side-effect-free predicate is preferable. If a guard changes state and then returns false, later cases observe changed state, so the path no longer depends only on the original subject.

When several cases perform almost the same action and differ only in how they extract data, an OR pattern may combine them. When they represent different business priorities, separate cases are easier to review. Do not turn a simple two-way Boolean decision into a complex pattern merely to use `match`.

### Names after a failed pattern

An implementation may make partial captures before the whole pattern eventually fails. The specification does not guarantee whether those partial bindings remain or are cleared, so later code must not read a name that a failed case might have captured.

Use capture names only inside the corresponding case suite to avoid implementation differences and stale values. If branches need to produce a shared result, assign that result explicitly in every successful branch and include a default branch for unknown input.

<!-- /deep -->

[Checkpoint: python/control-flow](https://codewiki.com/python/control-flow/#checkpoint)

## Further reading

- [Python 3.14 language reference: compound statements](https://docs.python.org/3.14/reference/compound_stmts.html)
- [Python 3.14 library reference: truth value testing](https://docs.python.org/3.14/library/stdtypes.html#truth-value-testing)
- [Python 3.14 tutorial: more control flow tools](https://docs.python.org/3.14/tutorial/controlflow.html)
- [PEP 634: structural pattern matching specification](https://peps.python.org/pep-0634/)
