Control flow determines which code runs next. Python maps data and program state to execution paths through conditions, loops, jump statements, and match.
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.
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
ifchain for mutually exclusive suites. - Use
forwhen an iterable input already exists instead of managing an unnecessary index. - Use
whilewhen repetition depends on changing state, and state the termination bound. - Consider
matchwhen a branch depends on both the shape and contents of nested data. - Use
returnto leave an entire function rather than treatingbreakas 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:
breakimmediately terminates the innermostfororwhileloop.continueskips the rest of this iteration and starts the innermost loop’s next one.returnterminates the whole function and gives an optional result back to its caller.passdoes 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.
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}')1. A-104: priority
2. B-205: hold
3. C-309: economy
4. D-410: standardThe 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.
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")camera: shelf 3
adapter: unavailableThe 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.
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")attempt 1: pending
attempt 2: pending
attempt 3: ready
job completedThe 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.
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))move to (3, 7)
move outside grid: (-1, 4)
message: deploy
unsupported eventstr(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
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
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
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
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
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.
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.
Further reading
4 questions · 1 predict-the-output · 1 spot-the-bug