🐍 Python Course · Stage 4 · Lesson 62 of 89 · Concept: Control Flow
Course Overview →

🟩 These concept pages connect Python to programming in general. Read Beginner tab first.

← Concept: Operators Concept: Loops →
thecodex.expert  ·  The Codex Family of Knowledge
CS Concepts

Control Flow

Programs can make decisions — execute different code depending on conditions. This is what makes them useful.

CS Concepts Requires: Variables ~16 min read Last verified:
Canonical Definition

Control flow refers to the order in which individual statements, instructions, and function calls are executed in a program — structured through conditional statements (if/else, switch/match) that select which branch of code executes based on the truth value of an expression, as opposed to the default sequential execution from top to bottom.

🟩 Beginner

Making programs make decisions

What you'll learn: The fundamental branching construct (if/else) and how "truthiness" works across languages. Switch/match for multiple choices. Ternary expressions for inline conditionals.

How to read this tab: This page explains the concept. The Python-specific syntax is covered in detail at Control Flow & Loops. Read both — this one gives you the why, the Python page gives you the how.

⏱ 20 min 📄 4 sections 🔶 Prerequisite: Variables + Operators
One sentence that captures it

Control flow is how a program decides which road to take at a fork — if this is true, go this way; otherwise, go that way.

If/else is universal — the syntax is all that varies. Python uses colons and indentation. Java/Go/JavaScript use curly braces. The concept is identical: evaluate a condition, execute one branch if true, another if false. Learning any language's branching syntax takes minutes once you understand the concept.

If / else: the fundamental branch

The if statement is the most fundamental control flow construct. It evaluates a condition. If the condition is true, it executes one block of code. If false, it skips to the optional else block.

Pythoncontrol_flow.py
score = 78

if score >= 90:
    grade = "A"
elif score >= 75:
    grade = "B"
elif score >= 60:
    grade = "C"
else:
    grade = "F"

print(f"Grade: {grade}")  # Grade: B

The conditions are evaluated in order from top to bottom. The first condition that evaluates to true causes its block to execute; all remaining branches are skipped. Only one branch runs.

💡

Python's match/case (3.10+) is more powerful than a switch. Most languages' switch matches on a single value. Python's match does structural pattern matching — it can match on the shape of data, not just a single value. match point: case Point(x=0, y=0): ... matches an object by its attributes. This is closer to Rust's match than to Java's switch.

Switch / match: choosing among many options

When you need to choose among many specific values (not ranges), a switch or match statement is cleaner than a long chain of if/elif. Python 3.10 introduced match with structural pattern matching. Java and C have switch. JavaScript has switch (with fall-through behaviour that requires explicit break statements).

Pythonmatch.py
command = "quit"

match command:
    case "start":
        print("Starting...")
    case "stop":
        print("Stopping...")
    case "quit" | "exit":       # match either
        print("Goodbye!")
    case _:                     # default — matches anything
        print(f"Unknown: {command}")
💡

Python's ternary reads left to right: value_if_true if condition else value_if_false. This is unusual — most languages (JavaScript, Java, C) use condition ? true_value : false_value. Python's order was chosen to read naturally: "I want X, if condition, otherwise Y." Use it for simple single-line choices; for anything complex, a regular if/else is clearer.

Ternary expressions: inline conditionals

Many languages offer a compact single-line conditional expression for simple cases. In Python: value_if_true if condition else value_if_false. In Java, C, JavaScript: condition ? value_if_true : value_if_false.

Multi-languageternary.txt
Python:     label = "adult" if age >= 18 else "minor"
Java/C/JS:  label = age >= 18 ? "adult" : "minor";
Rust:       let label = if age >= 18 { "adult" } else { "minor" };

Python's falsy values: False, None, 0, 0.0, "", [], {}, set(), (). Everything else is truthy. This lets you write if items: instead of if len(items) > 0:. Be careful: this only works if an empty container is genuinely the same as "no data". if value is None: is more explicit and often more correct than if not value: when you specifically mean None.

Truthiness: what counts as true?

In Python, JavaScript, and many other languages, values that are not booleans can be used in conditions. This is called truthiness. In Python, the following values are falsy: None, False, 0, 0.0, empty strings "", empty lists [], empty dicts {}. Everything else is truthy. In C, zero is false; any nonzero integer is true.

🔵 Intermediate

Structured programming, guard clauses, and pattern matching

What you'll learn: The structured programming theorem (why goto is harmful and sequence/selection/iteration is sufficient). Guard clauses as a cleaner alternative to deeply nested if/else. Pattern matching in Python, Rust, and Swift.

How to read this tab: The guard clause pattern is immediately useful in Python. Read the section and then refactor one of your functions that has deeply nested if/else — flatten it using early returns.

⏱ 20 min 📄 3 sections 🔶 Prerequisite: Beginner tab
📎

Dijkstra's 1968 letter "Go To Statement Considered Harmful" argued that goto makes programs impossible to reason about. The structured programming theorem (Böhm and Jacopini) proved that any program can be written using only sequence, selection (if/else), and iteration (loops) — no goto needed. This is why modern languages don't have goto (Python included).

Structured programming and the control flow theorem

Before structured programming (1960s), programs used arbitrary GOTO jumps — control could leap to any line at any time. Dijkstra's 1968 paper argued that well-structured programs should use only three control flow patterns: sequence (execute in order), selection (if/else, switch), and iteration (loops). Böhm and Jacopini proved these three are sufficient for any computable function. Modern languages reflect this: GOTO is gone from Python, Java, and Rust entirely, and discouraged in C and Go.

💡

Guard clauses flatten nested if/else. Instead of: def process(x): if x is not None: if x > 0: if x < 100: return do_work(x) — write: def process(x): if x is None: return; if x <= 0: return; if x >= 100: return; return do_work(x). The happy path has no indentation. Each guard handles one failure case early.

Short-circuit and guard clauses

A common pattern in structured programming is the guard clause: check for error conditions at the top of a function and return early if they're present, rather than nesting all the main logic inside an if block. This produces flatter, more readable code.

Pythonguard_clause.py
# Deep nesting — hard to read
def process(user, data):
    if user is not None:
        if user.is_active:
            if data is not None:
                return data.process()

# Guard clauses — flat and readable
def process(user, data):
    if user is None: return None
    if not user.is_active: return None
    if data is None: return None
    return data.process()
📎

Pattern matching is match + destructuring + binding in one. match command.split(): case ["quit"]: quit(); case ["go", direction]: move(direction); case ["get", item, *rest]: pick_up(item, rest). This replaces complex if/isinstance/indexing chains with clear, declarative patterns.

Pattern matching (Python 3.10+, Rust, Swift)

Modern match statements in Python, Rust, and Swift go beyond simple value comparison — they support structural pattern matching: destructuring tuples, matching object types and their attributes, binding values from matched structures. Rust's match is exhaustive — the compiler requires every possible variant to be handled, preventing missed cases.

Rustmatch.rs
enum Shape {
    Circle(f64),          // radius
    Rectangle(f64, f64),  // width, height
}

fn area(shape: Shape) -> f64 {
    match shape {
        Shape::Circle(r) => std::f64::consts::PI * r * r,
        Shape::Rectangle(w, h) => w * h,
        // Compiler error if any variant is missing — exhaustive
    }
}
Commonly confused
if x = 5 is assignment, not a condition. In Python this is a SyntaxError. In C and JavaScript, it compiles and assigns 5 to x, making the condition truthy. This is one of the most common C bugs. Python's := walrus operator intentionally requires parentheses to make this distinction explicit.
Switch fall-through in C and JavaScript is not a bug — it is a feature. In C and JavaScript switch, if a case doesn't end with break, execution falls through to the next case. This is intentional (allows sharing code between cases) but frequently a source of bugs. Python's match and Java's switch expressions do not fall through.
Python's elif is not the same as nested if. A chain of if/elif/elif/else is a single multi-branch structure — exactly one branch executes. A sequence of if/if/if statements checks each condition independently — multiple branches can execute.
How this connects
Requires first
🔴 Expert

Formal semantics of branching and compiled control flow

What you'll learn: The formal denotational semantics of if/else. How compilers translate conditional branches to machine code (conditional jump instructions). Branch prediction in CPUs.

How to read this tab: Read when studying compilers, formal verification, or CPU architecture.

⏱ 20 min 📄 2 sections 🔶 Prerequisite: After Stage 2

Formal semantics of if/else

In formal operational semantics, the if statement has two reduction rules:
if true then S₁ else S₂ → S₁
if false then S₁ else S₂ → S₂
The condition expression is evaluated first; the result selects which branch reduces. This formalises the intuition that exactly one branch executes.

Control flow in compiled code

At the machine code level, an if/else compiles to a conditional branch instructionJE (jump if equal), JNZ (jump if not zero), etc. on x86. The condition sets flags in the status register; the branch instruction reads those flags to decide whether to jump. Modern CPUs use branch prediction to speculatively execute the likely branch before the condition is resolved. Mispredictions (the speculated branch was wrong) cause a pipeline flush — a ~15–20 cycle penalty. This is why replacing an unpredictable branch with branchless arithmetic (e.g. using bitwise operations) can improve performance in inner loops.

Specification reference

Python match statement specification: PEP 634 — Structural Pattern Matching. peps.python.org/pep-0634/. Dijkstra, E. W. (1968). "Go To Statement Considered Harmful." CACM 11(3). Rust match exhaustiveness: The Rust Reference §8.9 — Match expressions. doc.rust-lang.org/reference/expressions/match-expr.html.

How this connects
Requires first
Source confidence: High Last verified: Primary source: Dijkstra, E. W. (1968). Go To Statement Considered Harmful. CACM 11(3).

✅ Expert tab complete

  • I understand at a high level how an if/else becomes conditional jump instructions in assembly
  • I know what CPU branch prediction is and why consistently-taken branches are faster

Continue to Loops →

Sources

1
Dijkstra, E. W. (1968). "Go To Statement Considered Harmful." CACM, 11(3), 147–148. — Foundation of structured control flow.
2
Python Software Foundation. PEP 634 — Structural Pattern Matching. peps.python.org/pep-0634/. — Python 3.10 match statement specification.
3
The Rust Reference, §8.9 — Match expressions. doc.rust-lang.org/reference/. — Exhaustive pattern matching specification.
4
ISO/IEC 9899:2018 (ISO C17). §6.8 — Statements and blocks. — Formal C if and switch semantics.