Skip to main content

Chapter 50: Control Flow

James opens a SmartNotes function that Claude Code generated for him. He has seen plenty of AI output by now: typed variables, collection operations, function signatures with docstrings. But this function is different. Halfway through the body, there is an if statement. Two branches, two return paths, two possible outcomes depending on a value he cannot predict just by reading top to bottom.

He stares at the branches. He can see what the code does on the left path. He can see what it does on the right. But he is not sure which path runs for a given input, and he definitely cannot picture what happens when the function sits inside a loop that calls it fifty times.

Emma leans over. "Remember when you could only read that if back in Chapter 49? You could trace what the branches meant, but you could not write one yourself. That changes today." She pauses. "You own it now."

This chapter removes the straight-line constraint from Phase 2. Up to this point, every function you have written executes one statement after another, top to bottom, no detours. Real functions are not like that. Real functions branch: "if the note is a draft, do this; otherwise, do that." Real functions loop: "for every tag in the list, check whether it matches." The combination of branching and looping is what turns a simple calculation into a program that can handle unpredictable input.

Why Control Flow Matters for Specification

In Chapter 49, you learned to write function signatures as contracts: name, parameters, return type, docstring. Those contracts describe WHAT a function does. Control flow describes HOW it decides. When you write def categorize_note(word_count: int) -> str, the signature promises a string back. But the signature says nothing about the logic that picks "short", "medium", or "long" based on the word count. That logic lives in if/elif/else branches inside the body.

Testing becomes more interesting too. A straight-line function has one path: call it, check the result. A branching function has multiple paths. A function with three elif branches has four possible outcomes, and you need a test for each one. By the end of this chapter, you will think about functions not as single pipes but as decision trees, and you will write tests that walk every branch.

What You Will Learn

By the end of this chapter, you will be able to:

  • Write if/elif/else branches that select between multiple outcomes based on typed conditions
  • Iterate over collections with for loops using range() and enumerate()
  • Mutate lists with .append(), .pop(), .sort(), and .remove()
  • Control loop execution with while, break, continue, and pass
  • Trace nested loops using trace tables and predict which break exits which loop
  • Write tests that cover every branch and boundary in a function

Chapter Lessons

LessonTitleWhat You DoDuration
1Branches: if/elif/elseWrite branches, test which path runs, chain comparisons25 min
2For Loops: Iteration BasicsIterate collections, use range(), accumulate with +=25 min
3enumerate and Collection OperationsUse enumerate(), .append()/.pop()/.sort()/.remove()20 min
4While Loops and Flow ControlWrite while loops, use break, continue, pass, sentinel values20 min
5Nested Loops and PatternsNest for loops, build trace tables, understand that break exits inner only, flatten nested structures25 min
6Testing Every PathLearn branch coverage (conceptual), write boundary tests20 min
7SmartNotes Control Flow TDGComplete a full TDG with branching and looping SmartNotes functions15 min
8Chapter 50 Quiz50 scenario-based questions covering all control flow concepts25 min

PRIMM-AI+ in This Chapter

Every lesson includes a PRIMM-AI+ Practice section following the five-stage cycle from Chapter 42. This is Phase 3: you are now WRITING control flow, building on the types (Phase 2) and function signatures (Chapter 49) you already own.

StageWhat You DoWhat It Builds
Predict [AI-FREE]Predict which branch executes or what a loop accumulates, with a confidence score (1-5)Calibrates your control flow intuition
RunExecute the code or run pytest, compare to your predictionCreates the feedback loop
InvestigateWrite a trace artifact explaining why a branch was taken or a loop produced its resultMakes your branching and looping reasoning visible
ModifyChange a condition or loop bound and predict the new resultTests whether your understanding transfers
Make [Mastery Gate]Write a branching or looping function from scratch with tests for every pathProves you can specify control flow independently

Syntax Card: Chapter 50

Reference this card while working through the lessons. Every construct shown here appears in at least one lesson.

# ── Branches ──────────────────────────────────────────
if temperature > 30:
status = "hot"
elif temperature > 15:
status = "warm"
else:
status = "cold"

# Chained comparison (Python shortcut)
if 0 < score <= 100:
print("Valid score")

# ── For Loop ──────────────────────────────────────────
for tag in tags:
print(tag)

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

for i in range(2, 8): # 2, 3, 4, 5, 6, 7
print(i)

for i in range(0, 10, 2): # 0, 2, 4, 6, 8
print(i)

# ── enumerate ─────────────────────────────────────────
for index, item in enumerate(items):
print(f"{index}: {item}")

# ── Collection Operations ─────────────────────────────
notes: list[str] = ["alpha", "beta"]
notes.append("gamma") # Add to end
notes.pop() # Remove last
notes.sort() # Sort in place
notes.remove("alpha") # Remove first match

# ── While Loop ────────────────────────────────────────
count: int = 0
while count < 10:
count += 1

# ── break / continue / pass ──────────────────────────
for item in items:
if item == "stop":
break # Exit loop immediately
if item == "skip":
continue # Skip to next iteration
pass # Placeholder (do nothing)

# ── Accumulator Pattern ──────────────────────────────
total: int = 0
for value in values:
total += value

# ── Nested Loops ─────────────────────────────────────
for row in matrix:
for cell in row:
print(cell)
# break inside the inner loop exits only the inner loop

Prerequisites

Before starting this chapter, you should be able to:

  • Write type annotations for int, float, str, bool, None (Chapter 47)
  • Write list[str], dict[str, str], and other collection types (Chapter 48)
  • Write function signatures with default parameters and docstrings (Chapter 49)
  • Complete full TDG (Type-Driven Generation) cycles: stub, test, generate (Chapter 46)
  • Read if statements in AI-generated code (Chapter 49 Lesson 2 preview)

The SmartNotes Connection

At the end of this chapter, you will tackle a full TDG challenge using SmartNotes functions that require control flow. You will write stubs and tests for three functions:

  • categorize_note(word_count: int) -> str: Uses if/elif/else to return "short", "medium", or "long" based on word count thresholds.
  • count_tags(notes_tags: list[list[str]]) -> dict[str, int]: Loops through nested tag lists, counts how many times each tag appears across all notes.
  • filter_notes_by_tag(notes: list[dict[str, str]], tag: str) -> list[dict[str, str]]: Iterates and accumulates matching notes into a new list.

Notice the type signatures: list[dict[str, str]] is verbose and hard to read. Every function that touches notes repeats this clunky type. You will feel the friction. That friction is intentional; it sets up Chapter 51, where dataclasses let you replace dict[str, str] with a clean Note type. For now, the pain continues.