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
| Construct | When to Use |
|---|---|
if / elif / else | Branching on conditions |
A if cond else B | Pick between two values in an expression |
for x in iter | Iterate over a known iterable |
while cond | Loop with no known iteration count |
break | Exit the current loop |
continue | Skip to next iteration |
for … else | "Did the loop complete without break?" |
match / case | Pattern-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
fordoesn't capture outer variables by value. Use default arguments or factor out the inner work into a function. - Confusing
and/orwithif/else:x and y or zis a bug-prone old idiom for ternary — usey if x else z. - Falsy gotcha:
if data:treats0,"", andNoneidentically. If you need to distinguish, useif data is not None:. - Ranged off-by-one:
range(stop)is0tostop-1inclusive. For 1..n inclusive, userange(1, n+1).