Lesson 06 / 18
Conditional Branching
Condition-dependent execution, multi-way selection, writing readable conditions, and mapping-based dispatch.
Contents
The programs written so far have followed a single path from start to end: each statement executed once, in its turn. Most problems, however, require different behavior depending on the situation. If a measurement is above a limit, a warning should be given; if it is below, nothing should happen.
This lesson covers how flow splits based on a condition. The previous course showed its hardware counterpart: a comparison instruction sets flags, and a conditional branch instruction changes the program counter only if the condition holds. Every construct here ultimately reduces to those instructions.
One-Way and Two-Way Selection
The simplest form is a block that executes only when a condition holds:
measurement = 25 LIMIT = 20 if measurement > LIMIT: print("limit exceeded") # limit exceeded
A condition is any expression that produces a logical value. If it does not hold, the block is skipped and flow continues below it.
In two-way selection, a block to run when the condition does not hold is also defined:
if measurement > LIMIT: status = "high" else: status = "normal" print(status) # high
Which statements a block covers is shown differently by language: some use curly braces, some close the block with a keyword, Python uses indentation. The notation varies; the meaning is the same — the set of statements to execute depending on the condition is marked off.
In languages that use indentation, one warning applies: indentation is not mere formatting but meaning. A statement believed to be inside the block but whose indentation has shifted always runs regardless of the condition. This is a common instance of the logic error class defined in the first lesson.
Multi-Way Selection
When there are more than two cases, conditions are chained. In the chain, the first condition that holds wins; the rest are never tested.
def classify(measurement: int) -> str: if measurement < 10: return "low" elif measurement < 20: return "medium" elif measurement < 30: return "high" else: return "extreme" print(classify(7), classify(12), classify(25), classify(40)) # low medium high extreme
The order of the chain determines the meaning of the conditions. The second branch’s
condition measurement < 20, read on its own, is also true for the value ; but by
the time it is reached, the first condition is known not to hold, meaning
measurement is at least . Every branch implicitly carries the negation of all
the ones before it.
This is why reordering breaks the program. The following chain always produces “extreme”, because its first condition is true for nearly every value:
def classify_wrong(measurement: int) -> str: if measurement >= 10: # broad condition placed first return "extreme" elif measurement < 20: return "medium" else: return "low" print(classify_wrong(12)) # extreme — expected "medium" print(classify_wrong(25)) # extreme — correct for this value
The practical rule: conditions are ordered from the narrowest case to the broadest;
the most general case is collected in the else branch. If the chain has no else
branch at the end, input that satisfies no condition passes through silently, doing
nothing — this is where overlooked cases typically hide. The last branch of every
chain should state explicitly what happens for unexpected input.
Writing Readable Conditions
As a condition grows more complex, code readability drops quickly. Three habits prevent this.
Using the logical value directly. Writing if condition == True is unnecessary;
condition is already a logical value. Adding the comparison lengthens the expression
and risks accidentally turning into an assignment.
Naming the condition. A long condition is bound to a variable that states its meaning:
measurement, temperature = 25, 80 # Hard to read: if measurement > 20 and temperature > 75 and not (measurement > 40): print("warning") # Named: limit_exceeded = measurement > 20 too_hot = temperature > 75 within_safe_range = measurement <= 40 if limit_exceeded and too_hot and within_safe_range: print("warning") # warning
Simplifying negation. Nested negations are flattened using De Morgan’s laws: the
expression not (a and b) is equivalent to (not a) or (not b). Which of the two
forms to choose is decided by which one states the problem more directly.
The Conditional Expression
Conditional branching is a statement: it performs an action, it does not produce a value. Many languages, by contrast, also offer a form that produces a value depending on a condition. The difference between the two forms is a direct application of the statement–expression distinction from the first lesson.
measurement = 25 # Statement form: the status variable is bound separately in each branch. if measurement > 20: status = "high" else: status = "normal" # Expression form: a single binding, the condition selects the value. status = "high" if measurement > 20 else "normal" print(status) # high
The expression form improves readability in short cases where a value is bound to one of two options: the variable is written once, and the binding is visibly in a single place.
It also has a limit. If the branches are long, nested, or contain side effects, the expression form becomes unreadable. The criterion is this: if the condition selects a value, use the expression form; if it selects an action, use the statement form.
Nested Conditions
Conditions can be nested inside one another. As depth grows, reading gets harder; each level adds an assumption the reader must keep in mind.
Condition stacks more than three levels deep are usually a sign that two separate problems are being solved in the same place. The standard way to reduce depth is to rule out invalid cases first and exit early. This pattern is revisited under the name guard clause in the functions topic; its counterpart here is turning nested branches into a flat chain.
Mapping-Based Dispatch
If a condition chain is nothing but a single value compared against constants, many languages offer a dedicated construct for it — switch-based selection or pattern matching. The construct’s name and capabilities vary by language.
An approach that offers an alternative to the chain is moving the selection into data:
UNIT_FACTOR = {"mm": 0.001, "cm": 0.01, "m": 1.0} def to_meters(value: float, unit: str) -> float: if unit not in UNIT_FACTOR: raise ValueError(f"unknown unit: {unit}") return value * UNIT_FACTOR[unit] print(to_meters(25, "cm")) # 0.25 print(to_meters(1500, "mm")) # 1.5
This form’s advantage is that adding a new unit is a data change, not a code change. A condition chain grows longer with every new case; a mapping table stays the same. This difference becomes more pronounced as the number of decisions grows; in the software design curriculum, the same idea is expanded under the heading of replacing conditionals with polymorphism.
The selection criterion is this: if the cases are fixed and few, a condition chain is clear; if the cases grow or change at run time, a mapping is preferred.
Summary
- Conditional branching executes a block of code only when a condition holds; a condition is an expression that produces a logical value.
- Block boundaries are shown differently by language; in languages that use indentation, indentation is part of the meaning.
- In multi-way selection, the first condition that holds wins; every branch implicitly carries the negation of the ones before it, and reordering breaks the program.
- Conditions are made readable by naming them and simplifying negations; a logical
value is not compared with
== True. - When a single value is compared against constants, a mapping table can replace a growing condition chain.
Next Step
Branching determines which path flow takes, but it executes each path only once. The measurement sequence in the shared problem, however, requires the same work for every element. The next lesson covers loops, which execute the same block repeatedly, and the guarantee that a loop terminates.
To keep your progress and take notes, Log in
My notes
Log in to take notes.