Chapter Four

Control Flow

Learning Objectives
  1. Explain why Python uses indentation rather than braces to define code blocks
  2. Write conditional logic using if, elif, and else statements
  3. Distinguish between truthy and falsy values and apply them in boolean contexts
  4. Construct while loops and for loops, including the use of range(), break, and continue
  5. Describe structural pattern matching with match/case and identify where it improves on if/elif chains

Up to now, every program we have written has been a straight line — each statement executed once, in order, top to bottom. Real programs are not like that. They make decisions, repeat actions, skip over irrelevant sections, and branch into different paths depending on what is happening at runtime. The mechanisms that control which code runs and when are called control flow structures, and they are where programs start to become genuinely interesting.

Indentation as Syntax

Most programming languages use curly braces {} to define blocks of code that belong together. Python does not. In Python, the structure of your code is determined by indentation — the whitespace at the beginning of each line.

if temperature > 30:
    print("It is hot.")
    print("Stay hydrated.")
print("Have a good day.")

The two indented lines belong to the if block. The third print runs regardless of the temperature, because it is back at the original indentation level. The standard is four spaces per level of indentation. Not tabs, not two spaces, not eight — four. PEP 8 is clear on this, and virtually every editor can be configured to insert four spaces when you press Tab.

This design choice is divisive. Programmers coming from C or Java sometimes find it uncomfortable. But it has a powerful consequence: the visual structure of Python code always matches its logical structure. You cannot have misleading indentation because the indentation is the structure. Code that looks right is right; code that looks wrong is wrong. After a few weeks, most people stop noticing it entirely.

The colon at the end of the if line is required. It marks the beginning of a new block. You will see it again on for, while, def, class, and every other statement that introduces a block.

Conditional Statements

The if statement is the most fundamental control flow structure. It executes a block of code only if a condition is true.

age = 25

if age >= 18:
    print("You are an adult.")

If you need an alternative when the condition is false, add an else clause:

age = 15

if age >= 18:
    print("You are an adult.")
else:
    print("You are a minor.")

For multiple branches, use elif (short for "else if"). Python evaluates each condition in order and runs the first block that matches:

score = 73

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

print(f"Your grade is {grade}.")   # Output: Your grade is C.

You can chain as many elif blocks as you need. Only one block ever executes — the first whose condition is True. If none match and there is no else, nothing happens, and execution continues after the entire if structure.

Truthiness and Falsy Values

Python does not require conditions to be literally True or False. Any object can be used in a boolean context, and Python has clear rules about which values are considered falsy — treated as False — and which are truthy.

The falsy values are:

  • False itself
  • None
  • Zero of any numeric type: 0, 0.0, 0j
  • Empty sequences: "" (empty string), [] (empty list), () (empty tuple)
  • Empty mappings: {} (empty dict)
  • Empty sets: set()

Everything else is truthy. This means you can write clean, idiomatic conditions:

name = input("Enter your name: ")
if name:
    print(f"Hello, {name}!")
else:
    print("You did not enter a name.")

Instead of writing if name != "", you write if name. The effect is the same — an empty string is falsy — but the shorter form is what experienced Python developers expect to see.

While Loops

A while loop repeats a block of code as long as a condition remains true:

count = 5
while count > 0:
    print(count)
    count -= 1
print("Liftoff!")

This prints 5 4 3 2 1 Liftoff!, each on its own line. The loop checks the condition before each iteration. If the condition is false from the start, the body never executes.

The danger with while is the infinite loop — a loop whose condition never becomes false:

# Do not run this
while True:
    print("This never stops.")

Infinite loops have legitimate uses (event loops, servers, game loops), but you must include a way to break out. If you accidentally create one, press Ctrl+C to interrupt the program.

For Loops

The for loop in Python is fundamentally different from the C-style for (int i = 0; i < 10; i++). Python's for is a for-each loop: it iterates over the items in a sequence, one at a time.

fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
    print(fruit)

This prints each fruit on its own line. The variable fruit is assigned to successive elements of the list on each pass through the loop. You do not manage an index or check a termination condition — Python handles it.

You can iterate over strings (character by character), tuples, dictionaries, files (line by line), and many other objects. The for loop works with anything that is iterable, a concept we will explore more in later chapters.

The range() Function

When you do need to loop a specific number of times or generate a sequence of numbers, use range():

for i in range(5):
    print(i)
# Output: 0 1 2 3 4 (each on its own line)

range(5) produces the numbers 0 through 4 — five numbers in total. The start is inclusive, the stop is exclusive. This convention, borrowed from C's zero-based indexing, is consistent throughout Python.

range() accepts up to three arguments: range(start, stop, step).

for i in range(2, 10, 3):
    print(i)
# Output: 2 5 8

for i in range(10, 0, -1):
    print(i)
# Output: 10 9 8 7 6 5 4 3 2 1

A common pattern is combining range() with len() to iterate with indices:

colours = ["red", "green", "blue"]
for i in range(len(colours)):
    print(f"{i}: {colours[i]}")

This works, but the idiomatic Python approach is enumerate(), which gives you both the index and the value:

colours = ["red", "green", "blue"]
for i, colour in enumerate(colours):
    print(f"{i}: {colour}")

The result is identical, but the code is cleaner and less error-prone. Prefer enumerate() over range(len(...)).

Break, Continue, and Else on Loops

break exits a loop immediately, skipping any remaining iterations:

for number in range(100):
    if number > 5:
        break
    print(number)
# Output: 0 1 2 3 4 5

continue skips the rest of the current iteration and moves to the next one:

for number in range(10):
    if number % 2 == 0:
        continue
    print(number)
# Output: 1 3 5 7 9

Python has an unusual feature that most other languages lack: an else clause on loops. The else block runs after the loop finishes normally — that is, when it was not terminated by a break:

for n in range(2, 10):
    for d in range(2, n):
        if n % d == 0:
            break
    else:
        print(f"{n} is prime")
# Output: 2 is prime, 3 is prime, 5 is prime, 7 is prime

The else here is attached to the inner for loop. If the loop completes without hitting break, the number has no divisors and is therefore prime. If break fires, the else is skipped.

The naming is unfortunate — "else" on a loop confuses many programmers, and some have suggested it should have been called nobreak. But it is a genuinely useful feature for search patterns: "loop through candidates, break if you find one, otherwise do something."

Match Statements

Python 3.10 introduced structural pattern matching with the match and case keywords. If you have used switch in C or Java, this is Python's equivalent — but significantly more powerful.

command = "quit"

match command:
    case "start":
        print("Starting the engine.")
    case "stop":
        print("Stopping the engine.")
    case "quit":
        print("Goodbye.")
    case _:
        print(f"Unknown command: {command}")

The _ in the final case is a wildcard — it matches anything and serves as the default branch.

What makes match more than a fancy if/elif chain is that it can destructure values:

point = (3, 7)

match point:
    case (0, 0):
        print("Origin")
    case (x, 0):
        print(f"On the x-axis at {x}")
    case (0, y):
        print(f"On the y-axis at {y}")
    case (x, y):
        print(f"Point at ({x}, {y})")

Here Python unpacks the tuple and binds the values to variables x and y in the matching case. This is structural pattern matching — the pattern describes the shape of the data, and Python matches against it.

You can also match on types, add guard conditions with if, and combine patterns with | (or):

match value:
    case int(n) if n > 0:
        print(f"Positive integer: {n}")
    case int(n):
        print(f"Non-positive integer: {n}")
    case str(s):
        print(f"String: {s}")
    case _:
        print("Something else")

Match statements are not always the right tool. For simple value comparisons, if/elif is often clearer. But for complex branching on data shapes — parsing commands, handling different message types, processing structured records — match is remarkably expressive.

A Preview of Comprehensions

Before we move on, it is worth mentioning that Python has a concise syntax for creating sequences from loops called comprehensions. Here is a quick taste:

squares = [x ** 2 for x in range(10)]
print(squares)   # Output: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

This creates a list of squares in a single expression. Comprehensions combine the idea of a loop with the idea of building a collection, and they are one of the features Python programmers reach for most often. We will cover them fully in Chapter 6.

Control flow is where code comes alive. With if, for, and while, you can express any computation — any decision, any repetition, any branching path through a problem. The structures are simple, but their combinations are limitless. The programs you build with them are limited only by your imagination and, occasionally, by your indentation.