Skip to main content

Control Flow

Learn how to control the flow of your Python programs

Control flow in Python directs which lines execute and how many times. Three structures cover 95% of beginner coursework: `if/elif/else` branches, `for` loops over iterables, and `while` loops with a counter or sentinel. Graders at CS50P, CSE 163, and 6.100L test the same five patterns repeatedly, so learning the canonical form for each pays for the entire term.

1. if / elif / else: the 3-branch decision pattern

Python evaluates an `if/elif/else` chain top-down and runs **only the first branch** whose condition is `True`. Subsequent `elif` tests are skipped even if they would also evaluate true. This top-down short-circuit is why ordering matters: write the most specific test first, the most general (`else`) last.

The expression after `if` does not need parentheses, but the colon at the end is mandatory. Indentation is syntactic, not cosmetic. Mixing tabs and spaces raises `TabError`. PEP 8 mandates 4 spaces per level; every modern editor (VS Code, PyCharm, Jupyter) inserts 4 spaces when you press Tab inside a `.py` file.

For exactly two cases, Python provides a **conditional expression** (ternary): `value if test else other`. This compresses three lines into one and is the idiomatic style for assigning a value based on a single check. Reserve `if/elif/else` blocks for branches that contain multiple statements or side effects.

Example

                      
                        # Run with: python branches.py
# Letter-grade conversion: a canonical CS50P assignment

score = 87

# Ordered most-specific to most-general
if score >= 90:
    grade = "A"
elif score >= 80:
    grade = "B"
elif score >= 70:
    grade = "C"
elif score >= 60:
    grade = "D"
else:
    grade = "F"

print(f"Score {score} -> Grade {grade}")

# Ternary form: use when both branches are single expressions
status = "pass" if score >= 60 else "fail"
print(f"Status: {status}")

# Chained comparison reads like math
if 80 <= score < 90:
    print("solid B range")
                      
                    

2. for loops: iterate over anything iterable

A `for` loop in Python walks an **iterable**, not an integer range. Strings, lists, tuples, dicts, sets, files, and generators are all iterable. Writing `for c in "hello":` prints each character; writing `for line in open("data.txt"):` reads one line per iteration without loading the file into memory. This makes `for` the workhorse loop for 80% of homework code.

The `range(start, stop, step)` function generates integers without materializing a list. `range(10)` produces `0, 1, ..., 9`. `range(1, 11)` produces `1` through `10`. `range(10, 0, -1)` counts down from `10` to `1`. The stop value is **exclusive** in every form, which is the source of the off-by-one errors that plague every CS101 midterm.

Three iteration helpers from the stdlib appear in every well-written solution: `enumerate(items)` yields `(index, value)` pairs, `zip(a, b)` walks two iterables in lockstep, and `reversed(items)` walks in reverse. Combining them, `for i, (x, y) in enumerate(zip(xs, ys)):` is the standard form for "track index while walking two parallel lists".

Example

                      
                        # Run with: python forloops.py
# Iteration patterns every grader expects to see

# range() is exclusive at the upper bound
for i in range(5):       # 0 1 2 3 4 (not 5)
    print(i, end=" ")
print()

# enumerate gives (index, value) pairs
fruits = ["apple", "banana", "cherry"]
for i, fruit in enumerate(fruits, start=1):
    print(f"{i}. {fruit}")  # 1. apple, 2. banana, 3. cherry

# zip walks two iterables in lockstep, stops at the shorter one
names = ["Alice", "Bob", "Carol"]
scores = [88, 91, 79]
for name, score in zip(names, scores):
    print(f"{name}: {score}")

# Looping over a dict iterates KEYS by default
grades = {"Alice": "A", "Bob": "B"}
for key in grades:
    print(key, grades[key])

# Use .items() for key-value pairs (idiomatic)
for name, grade in grades.items():
    print(f"{name} earned {grade}")
                      
                    

3. while loops: run until a condition flips

A `while` loop runs as long as its condition stays `True`. The condition is **rechecked at the top** of each iteration; if it starts `False`, the loop body never executes. Use `while` when the iteration count is not known in advance: reading user input until a sentinel, polling a queue, retrying a network call.

The two failure modes of `while` are the same in every language. An **infinite loop** runs when the condition variable is not updated inside the body. An **off-by-one** runs one iteration too many or too few when the condition uses `<` instead of `<=`. Guard against both by writing the exit condition first, then the update step.

For reading input until the user enters a sentinel, the canonical Python pattern is `while True: ... if exit_condition: break`. This is preferred over `while exit_condition:` because the exit check happens after the read, which mirrors how stdin actually behaves. The `break` keyword exits the nearest enclosing loop; `continue` jumps to the next iteration.

Example

                      
                        # Run with: python whileloops.py
# Sentinel input pattern that handles EOF cleanly

# Pattern 1: counter-driven while
countdown = 5
while countdown > 0:
    print(countdown)
    countdown -= 1  # CRITICAL: update inside the body
print("liftoff")

# Pattern 2: sentinel-driven while (CS50P "Cash" / "Coke" style)
total = 0
while total < 100:
    try:
        deposit = int(input("Insert coin (cents): "))
    except (ValueError, EOFError):
        break  # exit cleanly on non-numeric or end-of-file
    if deposit <= 0:
        continue  # skip invalid entries without exiting
    total += deposit
    print(f"Total so far: {total}")

change = total - 100
print(f"Change owed: {change}")

# while/else runs the else block ONLY if no break fired
n = 7
divisor = 2
while divisor < n:
    if n % divisor == 0:
        print(f"{n} is composite")
        break
    divisor += 1
else:
    print(f"{n} is prime")
                      
                    

4. break, continue, and the loop-else clause

The `break` statement exits the **innermost** enclosing loop immediately. The `continue` statement skips the rest of the current iteration and re-tests the loop condition. Both apply to `for` and `while` identically. Use them sparingly; deep `if/elif/break` chains usually mean the loop body wants to be a separate function.

Python provides a feature most languages lack: the **`else` clause on loops**. A `for/else` or `while/else` runs the `else` block when the loop exits **normally**, meaning without hitting a `break`. This is the canonical pattern for "search and report not-found": loop through candidates, `break` on a match, fall through to `else` for the no-match case. Cleaner than a separate `found` flag.

Nested loops add the **labeled break** problem: `break` exits only the inner loop. Two options: refactor the inner block into a function and `return`, or set a sentinel flag checked by the outer loop. The function approach is preferred. Functions also make the autograder error trace easier to read.

Example

                      
                        # Run with: python breakcontinue.py
# Search-then-report using for/else (no flag variable needed)

students = [
    {"name": "Alice", "score": 85},
    {"name": "Bob", "score": 72},
    {"name": "Carol", "score": 91},
]
target_name = "Carol"

for student in students:
    if student["name"] == target_name:
        print(f"Found {target_name}: {student['score']}")
        break
else:
    # runs ONLY if the for loop never hit break
    print(f"{target_name} not in roster")

# continue: skip invalid entries instead of breaking
total = 0
for value in ["10", "20", "abc", "30", ""]:
    if not value.isdigit():
        continue  # skip non-numeric items, keep going
    total += int(value)
print(f"Sum of numerics: {total}")  # 60
                      
                    

5. Match statements: structural pattern matching (Python 3.10+)

Python 3.10 introduced `match/case` for structural pattern matching. Unlike a C-style `switch`, match patterns destructure values: extract fields from tuples, dicts, and class instances in the same step as branching. This replaces long `if isinstance(x, T):` chains in parser and tree-walking code.

A `match` expression compares the subject against each `case` pattern in order. The first matching pattern runs; remaining cases are skipped. Patterns are not values; they are templates. The literal pattern `case 0:` matches the integer `0`. The capture pattern `case x:` matches any value and binds it to `x`. The class pattern `case Point(x=0, y=y):` matches a `Point` instance with `x` equal to `0` and binds the `y` attribute.

The `_` wildcard pattern is the catch-all. Place it last to handle the "everything else" case. Without a wildcard, an unmatched subject falls through silently, which is rarely what you want. CS50P does not yet test `match` in problem sets, but DATA 100 and CS229 final projects use it for token classification and AST walks.

Example

                      
                        # Run with: python match_demo.py
# Pattern matching: cleaner than nested if/elif for shaped data

def describe(value):
    match value:
        case 0:
            return "zero"
        case int(n) if n < 0:                # guard with "if"
            return f"negative int: {n}"
        case int(n):
            return f"positive int: {n}"
        case [x, y]:                          # exactly two elements
            return f"pair: {x}, {y}"
        case [x, *rest]:                      # head + tail of any length
            return f"list starting with {x}, {len(rest)} more"
        case {"name": name, "age": age}:      # dict with these keys
            return f"person {name}, {age} years"
        case _:                                # wildcard, always last
            return "unknown shape"

print(describe(0))
print(describe(-5))
print(describe([1, 2]))
print(describe([1, 2, 3, 4]))
print(describe({"name": "Alice", "age": 30}))
                      
                    

Common pitfalls

Infinite loop because the condition variable is never updated inside the body: `while x > 0: print(x)` runs forever.

Add the update step (`x -= 1`) on the same indentation as the body. Read the loop bottom-up before running: condition update, then body.

Off-by-one with `range(start, stop)`: writing `range(1, 10)` when the spec asks for "1 through 10" produces only `1..9`.

Remember `stop` is exclusive. Use `range(1, 11)` for `1..10`. If counting from N, write `range(1, N+1)` and confirm with a quick `list(range(...))` in a REPL.

Modifying a dict during iteration: `for k in d: del d[k]` raises `RuntimeError: dictionary changed size during iteration`.

Iterate over a snapshot of the keys: `for k in list(d): del d[k]`. Same fix applies to sets.

`break` in a nested loop exits only the inner loop, not the outer one. Students add a flag variable that grows messy.

Extract the inner loop into a helper function and use `return`. Cleaner than flag variables; the function name documents the search intent.

Using `==` to compare singleton values like `None`: `if x == None:` works but is slower and PEP 8 flags it.

Use `is`: `if x is None:`. Same applies to `True` and `False`, though `if x:` is preferred over `if x is True:` for truthy tests.

Forgetting the colon after `if`, `for`, `while`, or `def` lines triggers `SyntaxError: expected ':'` and points at the next line.

The error message points one line past the actual problem. Move up one line; the missing colon sits there.

When to use control flow

Reach for control flow whenever code needs to branch on a value, repeat a block N times, or process items from a list, file, or stream. For decisions that depend on type or structure, `match` is cleaner than chained `isinstance` calls.

Get expert help

Stuck on a control flow assignment? These DMPH service pages match this topic:

Need Help?

Having trouble with this topic on an assignment? Our Python developers ship working code plus a walkthrough that helps you explain the code in class.