Part 1 · Foundations · Module 04
Control Flow
So far your programs have run straight from top to bottom. Real programs make decisions and repeat work: "if the password is wrong, ask again", "for every line in the file, count the words". Control flow is how you tell Python which statements to run, when, and how many times. With it, you can write genuinely useful programs.
- Use Boolean logic, truthiness, and short-circuit evaluation correctly
- Make decisions with
if/elif/elseand conditional expressions - Repeat work with
whileandforloops over ranges, strings, and lists - Control loops with
break,continue, and the loopelseclause - Use the walrus operator
:=where it makes code clearer - Write
matchstatements with literal, capture, sequence, mapping, class, OR, and guarded patterns - Plan an algorithm with a flowchart before writing code
Boolean logic and truthiness
Every decision a program makes comes down to a condition: an expression that is either true or
false. You met the Boolean values True and False and the comparison operators
(==, !=, <, <=, >, >=)
in M02. The logical operators combine conditions:
a | b | a and b | a or b | not a |
|---|---|---|---|---|
| True | True | True | True | False |
| True | False | False | True | False |
| False | True | False | True | True |
| False | False | False | False | True |
Truthiness
Python lets you use any value as a condition. Each value is either truthy (treated as true) or falsy (treated as false). The falsy values are a short list worth memorizing:
FalseandNone- Zero of any numeric type:
0,0.0,0j - Empty collections:
"",[],(),{},set(),range(0)
Everything else is truthy, including "0", "False", [0], and
-1. So the idiomatic way to check "is this list non-empty?" is simply if items:.
Short-circuit evaluation
and and or are lazy: they stop as soon as the answer is known. This is called
short-circuiting. a and b does not even look at b if a is
falsy; a or b skips b if a is truthy. They also return one of their
operands, not necessarily True or False:
a or breturnsaif it is truthy, otherwiseb. Useful for defaults:name = user_input or "Anonymous".a and breturnsaif it is falsy, otherwiseb. Useful as a guard:count > 0 and total / count > 10never divides by zero.
Remember: falsy means False, None, zero, or empty; and/or
short-circuit and return an operand.
Decisions: if, elif, else
An if statement runs its block only when its condition is truthy. elif ("else if")
adds more conditions, checked in order, and else catches everything that did not match. Python runs
the first matching branch and skips the rest.
temperature = 18
if temperature >= 30:
advice = "Shorts and sunscreen."
elif temperature >= 15:
advice = "A light jacket."
elif temperature >= 0:
advice = "A warm coat."
else:
advice = "Stay inside!"
print(advice)
Here is that decision as a flowchart, a diagram of the paths a program can take. Diamonds are decisions, rectangles are actions.
flowchart TD
S([start]) --> A{"temperature >= 30?"}
A -- yes --> R1["Shorts and sunscreen"]
A -- no --> B{"temperature >= 15?"}
B -- yes --> R2["A light jacket"]
B -- no --> C{"temperature >= 0?"}
C -- yes --> R3["A warm coat"]
C -- no --> R4["Stay inside!"]
R1 --> E([print advice])
R2 --> E
R3 --> E
R4 --> E
The conditional expression
When you just need to choose between two values, the conditional expression (sometimes called
the ternary operator) does it in one line: value_if_true if condition else value_if_false.
Repeating with while
A loop repeats a block of code. Each repetition is called an iteration. A
while loop repeats as long as its condition stays truthy; it checks the condition before every
iteration.
Step through and watch the condition being checked four times: three times it is true, the fourth time
(count is 0) it is false and the loop ends.
while is the right choice when you do not know in advance how many iterations you need: "keep asking
until the input is valid", "keep halving until the number is small".
Iterating with for and range
A for loop takes each item of an iterable (anything you can loop over: a string, a
list, a range, a file…) in turn, binds it to a loop variable, and runs the block. You do not manage a counter, so
there is nothing to forget.
range
range produces a sequence of integers without building a list in memory:
range(5): 0, 1, 2, 3, 4 (five numbers, starting at 0, stopping before 5)range(2, 6): 2, 3, 4, 5range(10, 0, -3): 10, 7, 4, 1 (a step, which can be negative)
Just like slices, the stop value is excluded. That makes range(len(s)) exactly the valid indexes of
s.
enumerate: index and item together
When you need the position as well as the item, use enumerate instead of indexing by hand:
for i in range(len(fruits)):
print(i, fruits[i])for i, fruit in enumerate(fruits):
print(i, fruit)The accumulator pattern
Many loops build up a result: start with an empty value, then update it each iteration. This is called the accumulator pattern.
break, continue, and loop else
breakleaves the loop immediately.continueskips the rest of this iteration and moves on to the next one.- A loop may have an
elseblock, which runs only if the loop finished without hittingbreak. Think of it as "no break". It is perfect for searches: "if I never found it, then…".
Nested loops
A loop inside a loop runs the inner loop completely for each iteration of the outer loop. With 3 outer
and 4 inner iterations, the inner body runs 12 times. break only leaves the innermost loop.
The walrus operator :=
The assignment expression name := value, nicknamed the walrus operator because
:= looks like a walrus's eyes and tusks, assigns and produces the value, so you can use it
inside a condition. Its most common use is a loop that reads until a stop signal:
line = input("> ")
while line != "quit":
print("You said", line)
line = input("> ") # duplicatedwhile (line := input("> ")) != "quit":
print("You said", line)Structural pattern matching: match/case
A match statement compares a value (the subject) against a series of
patterns, top to bottom, and runs the block of the first pattern that fits. It is far more than a
"switch" statement: patterns can check the shape of data and pull pieces out of it into names. This is
called destructuring.
Literal, OR, and wildcard patterns
Capture and sequence patterns
A bare name in a pattern is a capture pattern: it matches anything and binds it to that name. A
list-like pattern [a, b] matches a sequence of exactly that length, and *rest captures
any remaining items.
Guards
A guard is an if after a pattern; the case only matches if the guard is also true.
match point:
case (x, y) if x == y:
print("on the diagonal")
case (x, y):
print("somewhere else")
Mapping patterns
A dictionary-like pattern {"key": pattern} matches a dict that has those keys (extra
keys are allowed). Dictionaries are covered fully in M06; for now, read
{"type": "click", "x": 3} as "a record with fields". This is exactly how you handle JSON data from the
web.
Class patterns
A class pattern such as int() or str(s) checks the type of the subject.
Point(x=0, y=y) checks the type and its attributes. (You will write your own classes in
M10; here the @dataclass line creates a simple Point type
for us.)
Algorithmic thinking with flowcharts
An algorithm is a step-by-step procedure that solves a problem. Before writing a loop-heavy program, professionals sketch the algorithm: inputs, decisions, repetitions, and when to stop. A flowchart is one good way; plain-language steps (pseudocode) are another.
Here is the algorithm for this module's guessing game:
flowchart TD S([start]) --> P["pick secret number 1..100
attempts = 0"] P --> G["read a guess"] G --> V{"is it a valid number?"} V -- no --> M["print 'please enter a number'"] --> G V -- yes --> I["attempts += 1"] I --> C{"guess vs secret"} C -- "lower" --> L["print 'too low'"] --> G C -- "higher" --> H["print 'too high'"] --> G C -- "equal" --> W["print 'correct in N attempts'"] --> E([end])
Every arrow back to "read a guess" is a loop; every diamond is an if. Reading off the chart: we need a
while loop (we do not know how many guesses), a validity check, and an
if/elif/else comparison. Translating it:
Practice
Some exercises below ask you to complete a small function: a named block that receives inputs
(parameters) and sends back a result with return. Functions let the tests try many inputs.
You will learn them properly in M05; for now, write your code in the indented
body and return the answer.
Print the numbers 1 to 15, one per line, but print Fizz instead of
multiples of 3, Buzz instead of multiples of 5, and FizzBuzz for multiples of both.
Hint
Check the "both" case first (n % 15 == 0), otherwise a
multiple of 15 would be caught by the "Fizz" branch.
Show solution
for n in range(1, 16):
if n % 15 == 0:
print("FizzBuzz")
elif n % 3 == 0:
print("Fizz")
elif n % 5 == 0:
print("Buzz")
else:
print(n)
Complete letter_grade(score) so it returns "A" for 90 and
above, "B" for 80–89, "C" for 70–79, "D" for 60–69, and "F"
below 60. Scores outside 0–100 return "invalid".
Hint
Check for invalid scores first:
if not 0 <= score <= 100: return "invalid". Then the elif chain from high to low.
Show solution
def letter_grade(score):
if not 0 <= score <= 100:
return "invalid"
if score >= 90:
return "A"
elif score >= 80:
return "B"
elif score >= 70:
return "C"
elif score >= 60:
return "D"
return "F"
print(letter_grade(85))
Complete digit_sum(n) to return the sum of the decimal digits of a
non-negative integer, using a while loop with % and // (no strings).
digit_sum(9875) is 29; digit_sum(0) is 0.
Hint
n % 10 is the last digit and n // 10 removes
it. Loop while n > 0.
Show solution
def digit_sum(n):
total = 0
while n > 0:
total += n % 10
n //= 10
return total
Complete first_long_word(text, length): return the first word in
text with at least length characters, or None if there is none. Use a
for loop with break or return. Bonus: try writing it with a loop
else.
Hint
if len(word) >= length: return word
Show solution
def first_long_word(text, length):
for word in text.split():
if len(word) >= length:
result = word
break
else:
result = None
return result
Complete respond(command) using a match on
command.split():
"go north"→"Going north"(any single direction word)"take lamp key"→"Taking lamp, key"(one or more items, joined with", ")"quit"or"exit"→"Bye"""(no words) →"Say something""add 2 3"→"5": two whole numbers added (use a guard withisdigit())- anything else →
"Unknown command"
Hint
Use case ["take", first, *rest]: to require at least one
item, an OR pattern case ["quit" | "exit"]:, and
case ["add", a, b] if a.isdigit() and b.isdigit():.
Show solution
def respond(command):
match command.split():
case []:
return "Say something"
case ["go", direction]:
return f"Going {direction}"
case ["take", first, *rest]:
return "Taking " + ", ".join([first, *rest])
case ["quit" | "exit"]:
return "Bye"
case ["add", a, b] if a.isdigit() and b.isdigit():
return str(int(a) + int(b))
case _:
return "Unknown command"
Mini-project: a guessing game and a menu calculator
Quiz
Recap
- Falsy values:
False,None, zero, and empty collections; everything else is truthy. and/orshort-circuit and return one of their operands.if/elif/elseruns the first matching branch;a if cond else bchooses a value.whilerepeats while a condition holds; make sure something changes so it ends.forwalks any iterable;range(start, stop, step)excludes stop;enumerategives indexes.breakexits,continueskips ahead, loopelsemeans "no break".:=assigns inside an expression; use it to remove duplication.matchdestructures data with literal, capture, wildcard, sequence, mapping, class, OR, and guarded patterns.- Sketch the algorithm (flowchart or pseudocode) before coding anything with loops.
Going further
- More control flow tools (official tutorial)
- PEP 636: Structural pattern matching tutorial
- The match statement in the language reference
- PEP 572: Assignment expressions
- Truth value testing
- Next: M05 · Functions