Control Flow

Easy35 min read

if / elif / else

Why Pythonic Control Flow Matters

The Problem: Code that just runs top-to-bottom can only do one thing. Real programs need to branch, repeat, and react to data.

The Solution: Python's control-flow primitives (if/elif/else, for, while, match, comprehensions) are intentionally minimal and readable — no parentheses, no braces, just indentation.

Real Impact: Mastering them lets you express any algorithm clearly. The match statement (3.10+) adds pattern matching that's far more powerful than C-style switch.

Real-World Analogy

Think of control flow as the traffic signals of your program:

  • if/elif/else = the intersection where decisions are made
  • for / while = the roundabouts where you cycle through data
  • break / continue = exits and shortcuts to skip part of the cycle
  • match = the smart highway interchange that routes by shape, not just value

Python uses indentation — not braces — to delimit blocks. The conditional is written without parentheses (though they're allowed).

score = 87

if score >= 90:
    grade = "A"
elif score >= 80:
    grade = "B"
elif score >= 70:
    grade = "C"
else:
    grade = "F"

print(f"Grade: {grade}")  # Grade: B

Conditional Expressions (Ternary)

status = "adult" if age >= 18 else "minor"

# Chainable, but only do this for short, clear cases
label = "hot" if t > 30 else "warm" if t > 20 else "cool"

Chained Comparisons

Python lets you chain comparison operators naturally — closer to math than to other languages.

if 0 <= x < 100:           # equivalent to 0 <= x AND x < 100
    ...

if a == b == c:             # all three equal
    ...

Boolean Operators and Truthiness

Python's and, or, and not are short-circuit operators. They return one of their operands — not necessarily a bool.

# Short-circuit: stop evaluating once the result is known
if user and user.is_active:
    ...                          # safe — user.is_active not checked if user is None

# Return-the-operand semantics
print(None or "default")    # 'default'
print("hi" and "bye")      # 'bye'
print("" or "fallback")     # 'fallback'

Falsy Values Recap

These are falsy: False, None, 0, 0.0, "", [], (), {}, set(). Everything else is truthy. Use this idiomatically:

if items:                       # Pythonic — not `if len(items) > 0:`
    process(items)

if name:                        # tests non-empty string
    greet(name)

⚠️ The None trap

If you need to distinguish None from "empty" or 0, use is None explicitly. if x: treats None, 0, "", and [] all as false.

for Loops

Python's for iterates over any iterable — list, tuple, string, dict, set, generator, file, range, anything implementing __iter__. There is no C-style for (i=0; i<n; i++).

# Iterate over a list
for name in ["Alice", "Bob", "Carol"]:
    print(f"Hello, {name}")

# Iterate with range() — equivalent of C-style loop
for i in range(5):              # 0, 1, 2, 3, 4
    print(i)

for i in range(1, 10, 2):       # 1, 3, 5, 7, 9 (start, stop, step)
    print(i)

# Iterate over a string — gives characters
for ch in "hi":
    print(ch)

# Iterate over a dict — gives keys by default
user = {"name": "Alice", "age": 30}
for key in user:
    print(key, user[key])

# Often clearer: iterate over .items()
for key, value in user.items():
    print(f"{key}: {value}")

enumerate and zip

Two indispensable built-ins for clearer loops.

# enumerate — index + value, no manual counter
for i, name in enumerate(names, start=1):
    print(f"{i}. {name}")

# zip — iterate two (or more) iterables in lock-step
for name, score in zip(names, scores):
    print(f"{name}: {score}")

# zip stops at shortest. Use strict=True (3.10+) to error on mismatch.
for a, b in zip(xs, ys, strict=True):
    ...

while Loops

Use while when you don't know how many iterations you'll need. For known counts, prefer for.

count = 0
while count < 5:
    print(count)
    count += 1

# Loop until a condition
while True:
    line = input("> ")
    if line == "quit":
        break
    process(line)

break, continue, and the loop-else clause

# break exits the loop entirely
for i in range(100):
    if found(i):
        break

# continue skips to the next iteration
for n in numbers:
    if n < 0:
        continue            # skip negatives
    process(n)

# Loop else — runs only if the loop completed without break
for n in numbers:
    if n == target:
        print("found")
        break
else:
    print("not found")        # only reached if no break

Loop-else is unusual

It's a polarizing feature. Read it as "if the loop ran to exhaustion" rather than "otherwise." It's especially useful for search loops to handle the "not found" case cleanly.

match (Python 3.10+)

Structural pattern matching is much more than a switch statement. It can destructure tuples, dicts, lists, and class instances, bind names, and apply guards.

Basic match

def http_response(status: int) -> str:
    match status:
        case 200:
            return "OK"
        case 301 | 302:                  # OR pattern
            return "Redirect"
        case 404:
            return "Not Found"
        case _:                          # wildcard / default
            return "Unknown"

Destructuring Patterns

def describe(point):
    match point:
        case (0, 0):
            return "origin"
        case (x, 0):                     # binds x
            return f"on x-axis at {x}"
        case (0, y):
            return f"on y-axis at {y}"
        case (x, y) if x == y:           # guard clause
            return f"diagonal at ({x}, {y})"
        case (x, y):
            return f"({x}, {y})"

Class Patterns

from dataclasses import dataclass

@dataclass
class Circle:
    radius: float

@dataclass
class Rectangle:
    width: float
    height: float

def area(shape) -> float:
    match shape:
        case Circle(radius=r):
            return 3.14159 * r * r
        case Rectangle(width=w, height=h):
            return w * h

Comprehensions

Comprehensions build collections in one expression. They're more concise — and usually faster — than the equivalent for loop with append.

List Comprehensions

# Build a list of squares
squares = [n * n for n in range(10)]
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# With a filter
evens = [n for n in range(20) if n % 2 == 0]

# Nested
matrix = [[1, 2], [3, 4]]
flat = [n for row in matrix for n in row]
# [1, 2, 3, 4]

Dict and Set Comprehensions

# Dict comprehension
lookup = {n: n * n for n in range(5)}
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# Set comprehension — deduplicates automatically
unique_lengths = {len(w) for w in words}

Generator Expressions

Same syntax but with parentheses — produces a lazy generator instead of materializing a list. Use them when feeding a function that consumes an iterable.

# No intermediate list — streams values
total = sum(n * n for n in range(1_000_000))

# Compare with list comp — builds 1M-element list first (wastes memory)
total = sum([n * n for n in range(1_000_000)])

When to skip the comprehension

If the loop body has side effects (printing, mutating state), use a plain for loop. Comprehensions are for building data, not for doing things.

Exception Flow (preview)

Exceptions are a form of control flow. We cover them in depth in the Exceptions tutorial, but the basic shape:

try:
    n = int(user_input)
except ValueError as e:
    print(f"not a number: {e}")
else:
    print(f"got {n}")             # runs only if no exception
finally:
    print("done")                  # always runs

Quick Reference

ConstructWhen to Use
if / elif / elseBranching on conditions
A if cond else BPick between two values in an expression
for x in iterIterate over a known iterable
while condLoop with no known iteration count
breakExit the current loop
continueSkip to next iteration
for … else"Did the loop complete without break?"
match / casePattern-match on structure (3.10+)
[expr for x in it]List comprehension
{k: v for …}Dict comprehension
{expr for …}Set comprehension
(expr for …)Generator (lazy) — feed to sum/min/max/any/all

🎯 Practice Exercises

Exercise 1: FizzBuzz

Print numbers 1 to 100, replacing multiples of 3 with "Fizz", multiples of 5 with "Buzz", and multiples of 15 with "FizzBuzz". Write it both with if/elif/else and with a match statement.

Exercise 2: Prime sieve

Use a list comprehension and a generator expression to build a list of all primes under 100.

Exercise 3: Word frequency

Given a string, build a dict comprehension that maps each unique word to its count. Compare with using collections.Counter.

Exercise 4: Pattern matcher

Write a function that takes a dict representing a JSON response and uses match to handle different response shapes: {"ok": True, "data": ...}, {"ok": False, "error": ...}, anything else.

Common Pitfalls

⚠️ Avoid These

  • Modifying a list while iterating it: Causes elements to be skipped silently. Build a new list (with a comprehension) or iterate over list(items).
  • Late binding in nested comprehensions: Inner for doesn't capture outer variables by value. Use default arguments or factor out the inner work into a function.
  • Confusing and/or with if/else: x and y or z is a bug-prone old idiom for ternary — use y if x else z.
  • Falsy gotcha: if data: treats 0, "", and None identically. If you need to distinguish, use if data is not None:.
  • Ranged off-by-one: range(stop) is 0 to stop-1 inclusive. For 1..n inclusive, use range(1, n+1).