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.
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.
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.
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).
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.
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.
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.
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.
# 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.
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
}
}
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, 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.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.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.
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 instruction — JE (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.
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.
✅ 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