S006 · Control Flow — if/else, loops, comprehensions
Every program is a giant decision tree.
Module M01: Python Foundations · Session 6 of 130 · Track: SW 🔁
What you'll be able to do after this session
- Explain Control Flow to a friend in one minute, out loud, no notes.
- Recognise it when you see it in real code / systems / papers.
- Complete the hands-on exercise at the end without looking up help.
Prerequisites
If you can't answer these in 30 seconds each, redo the linked session first:
Pre-read (skim before the session)
- 🎥 Intuition (5–15 min) · Python if/else, for, while — Programming with Mosh — Mosh's clean 15-min tour of the control-flow primitives.
- 🎥 Deep dive (30–60 min) · Python Loops and Iterators — Corey Schafer — The definitive 'for is really an iterator protocol' explainer.
- 🎥 Hands-on demo (10–20 min) · List, Dict, Set Comprehensions — Corey Schafer — Comprehensions are Python's superpower — 15 min well spent.
- 📖 Canonical article · Python Docs — More Control Flow Tools (§4) — Official tutorial; short and canonical.
- 📖 Book chapter / tutorial · Real Python — Python 'for' Loops (Definite Iteration) — Deep dive into
for,range, iterators,elseclause on loops. - 📖 Engineering blog · Google Python Style Guide — Language Rules — See how Google constrains control-flow patterns at massive scale.
(a) Intuition · 5 min
The one-paragraph mental model. Why this concept exists, what problem it solves, and the analogy that makes it stick.
In one sentence: Every program is a giant decision tree.
Every program that has ever run — from Excel to GPT — boils down to two ideas: do this, then that (sequence), and decide what to do next (control flow). Control flow is the branching + repeating structure that turns straight-line code into something that reacts to data.
Python gives you three primitives: if/elif/else (a decision), for (iterate over a known collection), and while (repeat until a condition changes). That's it. Everything more exotic — pattern matching, comprehensions, generators — is sugar on top.
The two-line insight that makes it click: for in Python is not a counter loop; it's a for-each loop over any iterable. You don't write for (i=0; i<n; i++); you write for user in users: — much more direct. When you do need an index, use enumerate(users). When you need to loop over two things in parallel, use zip(a, b).
Comprehensions ([x*2 for x in xs if x > 0]) are the "Python signature move" — they replace 4-line loops with one line that reads left-to-right like English. Master them and your code will look native.
Gotcha to carry forever: else on a for loop runs only if the loop finished without hitting break. This is one of Python's most surprising features and once you know it, you'll use it for search patterns. Also: never modify a list while iterating over it — either build a new list or iterate over a copy.
(b) Visual walkthrough · 15 min
Diagrams, worked examples, step-by-step visuals.
Control flow at a glance:
The decision table — which construct fits which problem:
| You want to… | Use | Sketch |
|---|---|---|
| Branch on a condition | if / elif / else | if x > 0: … elif x == 0: … else: … |
| Iterate a known collection | for | for user in users: … |
| Iterate + know the index | enumerate | for i, u in enumerate(users): … |
| Iterate two lists in lockstep | zip | for a, b in zip(xs, ys): … |
| Repeat until a condition changes | while | while not done: … |
| Build a new list from an old one | list comprehension | [f(x) for x in xs if pred(x)] |
| Build a dict | dict comprehension | {k: v for k, v in pairs} |
| Deduplicate | set comprehension | {f(x) for x in xs} |
| Lazy sequence (memory-cheap) | generator expression | (f(x) for x in xs) |
| Multi-way branch on shape | match (Py 3.10+) | match cmd: case "go": … |
Worked example 1 — the FizzBuzz classic (bad → good):
# Beginner version
for i in range(1, 16):
if i % 15 == 0:
print("FizzBuzz")
elif i % 3 == 0:
print("Fizz")
elif i % 5 == 0:
print("Buzz")
else:
print(i)
# Pythonic one-liner using comprehension + conditional expression
print("\n".join(
"FizzBuzz" if i % 15 == 0 else "Fizz" if i % 3 == 0 else "Buzz" if i % 5 == 0 else str(i)
for i in range(1, 16)
))Worked example 2 — search with for/else:
users = [{"name": "alice"}, {"name": "bob"}, {"name": "carol"}]
target = "dave"
for u in users:
if u["name"] == target:
print("found", u)
break
else:
print(target, "not in list") # runs only if the loop completed with no breakfor/else is the cleanest way to express "search a collection; do X if not found" without a found = False flag.
(c) Hands-on · 20 min
Code you type and run. Not pseudo-code — real, runnable, copy-pasteable.
Ten miniature exercises — type each one and read the output. This should take ~20 minutes.
# s006_control_flow.py
# 1. if / elif / else
def grade(pct: int) -> str:
if pct >= 90: return "A"
elif pct >= 75: return "B"
elif pct >= 50: return "C"
else: return "F"
for p in (95, 80, 60, 30):
print(p, "->", grade(p))
# 2. for over a list
fruits = ["apple", "banana", "cherry"]
for f in fruits:
print("i like", f)
# 3. enumerate for index + value
for i, f in enumerate(fruits, start=1):
print(f"{i}. {f}")
# 4. zip for parallel iteration
prices = [50, 20, 300]
for f, p in zip(fruits, prices):
print(f"{f}: ₹{p}")
# 5. while with a real exit condition
import random
random.seed(42)
rolls = 0
while (r := random.randint(1, 6)) != 6:
rolls += 1
print(f"rolled {rolls} times before getting a 6")
# 6. list comprehension replacing a loop
squares = [x * x for x in range(10)]
print(squares)
# 7. filtering with a comprehension
evens = [x for x in range(20) if x % 2 == 0]
print(evens)
# 8. dict + set comprehensions
name_len = {f: len(f) for f in fruits}
print(name_len)
first_letters = {f[0] for f in fruits}
print(first_letters)
# 9. for/else — search that reports "not found"
def find_user(users, target):
for u in users:
if u == target:
print("found", target)
break
else:
print(target, "not found")
find_user(["alice", "bob"], "bob")
find_user(["alice", "bob"], "carol")
# 10. match/case (Python 3.10+)
def http_status(code: int) -> str:
match code:
case 200: return "OK"
case 301 | 302: return "redirect"
case n if 400 <= n < 500: return "client error"
case n if 500 <= n < 600: return "server error"
case _: return "unknown"
for c in (200, 302, 404, 500, 999):
print(c, http_status(c))Checklist — what to observe:
- Exercise 1: no
switchin Python —elifchains are idiomatic. - Exercise 5 uses the walrus operator
:=(Py 3.8+) to assign inside thewhile. This is a common test-then-loop pattern. - Exercise 6's comprehension is faster than the equivalent
for+appendloop in CPython. - Exercise 9 shows
for/else: the "not found" branch fires only when nobreakhappened. - Exercise 10's
matchuses guards (case n if …) and OR patterns (301 | 302).
Try this modification: rewrite Exercise 1 with a match statement using range guards. Then convert Exercise 6 into a generator expression ((x*x for x in range(10))) and observe what changes when you print it (spoiler: you see a generator object; wrap it in list() to materialise).
(d) Production reality · 10 min
How this shows up in real systems, gotchas, war stories, common bugs.
War story — the O(n²) list-in-a-loop. A data pipeline at a hedge fund did if x not in seen: seen.append(x) inside a loop over 500 000 items. in on a list is O(n), so the pipeline was O(n²) — 40 minutes to run. Changing seen from list to set (which has O(1) in) dropped it to 2 seconds. Rule: whenever you use in, ask "is this a set or dict?" If not, it might need to be.
War story — the modified-while-iterating crash. A dev at a startup wrote for u in users: if u.expired: users.remove(u). Python silently skipped every other user; some expired users survived. Correct patterns: iterate a copy (for u in users[:]:), build a new list (users = [u for u in users if not u.expired]), or iterate indices in reverse (for i in range(len(users)-1, -1, -1): …).
War story — the infinite loop in prod. A while queue: process(queue.pop()) looked innocent, but process sometimes added to the queue. Under load, the queue grew faster than it shrank — infinite loop, 100 % CPU, pager storm at 2 a.m. Rule: every while needs a provable termination argument (a decreasing counter, a bounded external condition, a timeout). Prefer for _ in range(MAX_ITERATIONS): with an inner condition when you can.
How top teams write control flow:
- Early returns > deep nesting. Guard clauses (
if not user: return None) beat 4-levelifpyramids. Google's style guide explicitly recommends this. - Comprehensions for transforms; loops for side effects. A comprehension that
prints is a code smell. match/casefor state machines and parsers. Not for every 3-way branch —if/elifis often clearer.- Prefer
any()/all()over hand-rolled search loops.if any(u.expired for u in users)is one line and short-circuits. - Type-hint the collection.
def process(users: list[User]) -> list[User]:makes the loop's contract obvious.
One counter-intuitive perf note: in tight numerical loops, a comprehension [f(x) for x in xs] is usually faster than map(f, xs) in CPython 3.11+, and both are slower than NumPy vectorisation. If a loop is your bottleneck, the answer is often "stop looping — vectorise."
(e) Quiz + exercise · 10 min
- What's the difference between a
forloop and awhileloop, and when do you prefer each? - What does the
elseclause on aforloop do, and when does it run? - Rewrite
result = []; for x in xs: if x > 0: result.append(x*2)as a single comprehension. - Why is
if item in my_listsometimes O(n) and sometimes O(1)? What's the fix? - Why is modifying a list while iterating over it a bug, and what are two safe alternatives?
Stretch problem (connects to S005): given logs = ["INFO ok", "ERROR db", "WARN slow", "ERROR net", "INFO ok"], use a single dict-comprehension + collections.Counter (or a plain loop) to produce {"INFO": 2, "ERROR": 2, "WARN": 1}. Then explain in one sentence why using a dict here beats using a list of (level, count) tuples.
Explain-out-loud test
If you can't teach these 3 things to a friend without notes, redo the session:
- What is Control Flow? (one sentence, no jargon)
- When would you use it? (one concrete example)
- When would you NOT use it? (one anti-pattern)
What comes next
- Next session (S007): Functions — arguments, scope, closures
- Previous (S005): Python Variables & Types — Mental Model of Memory
- Hub: The 6-Month Learning Plan
Part of a 130-session evergreen learning series. Session structure: (a) intuition · (b) visual walkthrough · (c) hands-on · (d) production reality · (e) quiz + exercise. Duration: 60 minutes.
More from M01 · Python Foundations
all modules →