Control Flow

This section documents the current Python release line as published at the official Python documentation, which is the reference these pages are written and verified against. No specific patch version is pinned.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Python has the usual conditional and looping constructs, plus, since 3.10, a structural pattern matching statement (match) that goes well beyond a simple switch. All of it is covered in the tutorial’s More Control Flow Tools chapter.

if / elif / else and Truthiness

Python has no switch before 3.10 and no ternary keyword — branching is if / elif / else, and every value has a truth value used wherever a condition is expected:

def describe(n):
    if n < 0:
        return "negative"
    elif n == 0:
        return "zero"
    else:
        return "positive"

An object is falsy if it is None, False, a numeric zero (0, 0.0, 0j), or an empty container ("", (), [], \{}, set(), range(0)) — everything else is truthy:

for value in [0, 1, "", "a", [], [0], None, {}]:
    print(value, "->", "truthy" if value else "falsy")

# 0 -> falsy      1 -> truthy    '' -> falsy   'a' -> truthy
# [] -> falsy     [0] -> truthy  None -> falsy {} -> falsy

This means if items: is the idiomatic emptiness check for any container — prefer it over if len(items) > 0:. The full rule set is documented under More on Conditions and Truth Value Testing.

while and for Loops

while repeats as long as its condition stays truthy; for iterates over any iterable — there is no C-style three-part for:

n = 5
while n > 0:
    print(n)
    n -= 1

for ch in "abc":
    print(ch)   # a, b, c

range()

range() generates an integer sequence lazily. Counting loops iterate over it rather than manually incrementing an index:

for i in range(5):        # 0, 1, 2, 3, 4
    print(i)

for i in range(2, 10, 2):  # start, stop, step -> 2, 4, 6, 8
    print(i)

Full parameters and behaviour are documented at the range type reference.

break, continue, pass

break exits the innermost loop immediately; continue skips to the next iteration; pass is a no-op statement used where syntax requires one, such as an empty body while stubbing something out:

for n in range(10):
    if n == 3:
        continue    # skip 3
    if n == 7:
        break       # stop entirely at 7
    print(n)        # 0, 1, 2, 4, 5, 6

def not_implemented_yet():
    pass    # placeholder body -- syntactically required, does nothing

The Loop else Clause

A for or while loop can carry an else block, executed only if the loop finished without hitting a break — a Python-specific idiom often used for "search, then report not-found":

def find_factor(n):
    for i in range(2, n):
        if n % i == 0:
            print(f"{n} = {i} * {n // i}")
            break
    else:
        print(f"{n} is prime")   # runs only when no break occurred

find_factor(15)   # 15 = 3 * 5
find_factor(13)   # 13 is prime

The loop else clause is documented alongside the loop statements in Break and continue Statements, and else Clauses on Loops.

The match Statement (3.10+)

match compares a subject against a series of case patterns, executing the first one that matches — structural pattern matching, not a value switch. It is specified in the language reference’s match statement section.

Literal, Capture, and Wildcard Patterns

def http_status(code):
    match code:
        case 200:
            return "OK"
        case 404:
            return "Not Found"
        case 500 | 502 | 503:      # combine literals with '|'
            return "Server Error"
        case other:                 # capture pattern: binds 'other' to the value
            return f"Unhandled: {other}"

A bare underscore _ is the wildcard pattern: it matches anything without binding a name, and conventionally goes last as the catch-all:

def sign(n):
    match n:
        case 0:
            return "zero"
        case n if n > 0:
            return "positive"
        case _:
            return "negative"

Guards

A case may carry an if guard — extra runtime logic that must also hold, evaluated only after the pattern itself matches:

def classify(point):
    match point:
        case (x, y) if x == y:
            return "on the diagonal"
        case (x, y) if x == 0 or y == 0:
            return "on an axis"
        case (x, y):
            return f"elsewhere: ({x}, {y})"

Class, Sequence, and Mapping Patterns

A sequence pattern destructures lists/tuples by position (rest collects the remainder); a mapping pattern destructures dicts by key, ignoring any keys not named (equivalent to always carrying an implicit rest); a class pattern* matches an instance’s type and unpacks its attributes:

from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

def describe(value):
    match value:
        case []:
            return "empty list"
        case [first, *rest]:
            return f"starts with {first}, then {rest}"
        case { "kind": "circle", "radius": r }:
            return f"circle of radius {r}"
        case Point(x=0, y=0):
            return "origin"
        case Point(x=x, y=y):
            return f"point at ({x}, {y})"
        case _:
            return "no match"

describe([1, 2, 3])                       # 'starts with 1, then [2, 3]'
describe({"kind": "circle", "radius": 2}) # 'circle of radius 2'
describe(Point(0, 0))                     # 'origin'

Dict/mapping patterns use \{ } just like dict literals — this is standard match syntax, not anything special to this page. The full pattern grammar (OR patterns, AS patterns binding a whole subpattern, nested class patterns) is in the same language-reference section.

if/elif/else vs. match: The Same Decision, Two Ways

Both constructs pick one branch out of several based on the subject’s shape or value — match simply lets the pattern itself express structure (class, sequence, mapping) that an if chain would otherwise have to spell out with isinstance() and manual indexing:

flowchart TD A[Start: classify shape] --> B{if/elif/else chain} B -->|isinstance Circle| C[compute pi * r ** 2] B -->|isinstance Square| D[compute side ** 2] B -->|else| E[raise ValueError] F[Start: classify shape] --> G{match shape} G -->|case Circle r=r| H[compute pi * r ** 2] G -->|case Square side=s| I[compute s ** 2] G -->|case _| J[raise ValueError]
# Equivalent if/elif/else
def area_if(shape):
    if isinstance(shape, Circle):
        return 3.14159 * shape.r ** 2
    elif isinstance(shape, Square):
        return shape.side ** 2
    else:
        raise ValueError("unknown shape")

# Equivalent match
def area_match(shape):
    match shape:
        case Circle(r=r):
            return 3.14159 * r ** 2
        case Square(side=s):
            return s ** 2
        case _:
            raise ValueError("unknown shape")

Reach for match when a value’s shape — its type, length, or set of keys — drives the branching; a plain if/elif/else chain remains clearer for simple value or range comparisons. See the tutorial’s control flow overview for how both fit alongside loops and function calls.

See Also

  • Functions — defining the functions called from inside these branches and loops.

  • Exceptions — handling errors that if checks and match guards can’t rule out ahead of time.