Control Structures
The three building blocks of every program — sequence, selection and iteration — and why structured programming replaced goto.
Three building blocks
A processor simply executes one instruction after another, unless an instruction tells it to jump somewhere else. Programming languages wrap those jumps in a few well-behaved patterns. A control structure is such a pattern: a language construct that decides which statements run, in which order, and how often.
In 1966, Corrado Böhm and Giuseppe Jacopini proved that three structures are enough to express any computation. This result is known as the structured program theorem:
- Sequence — do one step, then the next.
- Selection — choose between steps based on a condition.
- Iteration — repeat steps while a condition holds.
Each structure has one way in and one way out, so they can be nested
inside each other like boxes: a loop inside a branch inside a
sequence. Everything else — switch, for,
break — is convenience built on top of these three.
Sequence
Sequential execution is the default: statements run one after another, in the order they are written, each one finishing before the next begins. It is so natural that it is easy to forget it is a structure at all — but the order matters. Calculating a total before reading the quantity gives a wrong answer, even if every individual line is correct.
READ price
READ quantity
total ← price * quantity
PRINT total
Selection
Selection, also called branching, chooses which statements run based on a condition: an expression that is either true or false. Only the chosen branch runs; the others are skipped. How conditions are combined with AND, OR and NOT is covered in Boolean Logic.
if and if-else
The simplest form is a single IF: run a block only when
the condition is true, otherwise do nothing. Adding
ELSE gives two-way selection: exactly one of two blocks
runs.
IF basket_total ≥ 50 THEN
shipping ← 0
ELSE
shipping ← 4.95
END IF
else-if chains
For more than two outcomes, chain the conditions. They are tested
from top to bottom, and the first one that is true wins;
the final ELSE catches everything left over. Python
spells "else if" as elif, C and JavaScript as
else if.
for temperature in (35, 20, 5):
if temperature > 30:
label = "hot"
elif temperature > 15:
label = "mild"
else:
label = "cold"
print(temperature, label)
35 hot 20 mild 5 cold
Order matters: 35 is also greater than 15, but the chain never gets that far, because the first test already matched. Put the most specific conditions first. Python's version is described in detail in Control Flow.
switch and case
When one value is compared against a list of fixed options, many
languages offer a switch statement. The value is
evaluated once, and execution jumps to the matching
case label, or to default if none matches.
In C, C++, Java and JavaScript, a case label is only an
entry point. Execution continues into the following cases until it
reaches a break (or the end of the switch). This is
called fall-through. Used on purpose, it lets
several cases share code — here, each access level also gets
everything from the levels below it:
#include <stdio.h>
int main(void) {
int level = 2;
switch (level) {
case 3:
printf("Unlock the archive\n");
/* fall through */
case 2:
printf("Unlock the reports\n");
/* fall through */
case 1:
printf("Unlock the dashboard\n");
break;
default:
printf("No access\n");
}
return 0;
}
Unlock the reports Unlock the dashboard
More often, fall-through is a bug. Forget one break and
the next case runs as well:
const light = "yellow";
switch (light) {
case "green":
console.log("Go");
break;
case "yellow":
console.log("Slow down");
// break forgotten here
case "red":
console.log("Stop");
break;
default:
console.log("Unknown signal");
}
Slow down Stop
Because this mistake is so common, newer languages changed the rule.
C# refuses to compile a non-empty case that falls through, and Go
and Swift stop after each case unless you ask for fall-through
explicitly. Python had no switch at all until version 3.10 added the
match statement, which never falls through and can also
match the shape of data — see
Control Flow.
Iteration
Iteration, or looping, repeats a block of statements. Every loop needs something that eventually ends it: a condition that becomes false, a counter that reaches its limit, or a collection that runs out of items. A loop whose condition never becomes false runs forever. Loops come in four families, named after what controls them.
Pre-test loops: while
A WHILE loop tests its condition before each
pass. If the condition is false from the start, the body never runs
at all. Use it when you do not know in advance how many passes are
needed.
WHILE balance > 0
pay monthly installment
END WHILE
balance = 50
months = 0
while balance > 0:
balance -= 20
months += 1
print(months, balance)
3 -10
Post-test loops: do-while
A post-test loop runs the body first and tests the condition
afterward, so the body always runs at least once. That fits
situations like "show the menu, then repeat while the user has not
chosen Quit". C, C++, Java and JavaScript write it as
do { … } while (condition);. Some languages, such as
Pascal, have REPEAT … UNTIL instead, which stops when
its condition becomes true.
let stock = 0;
while (stock > 0) {
console.log("while: body ran, stock", stock);
stock--;
}
do {
console.log("do-while: body ran, stock", stock);
} while (stock > 0);
do-while: body ran, stock 0
Python has no do-while loop. The usual substitute is an endless
while True loop with the test at the end of the body:
readings = [12, 18, 25, 31]
i = 0
while True: # body first...
reading = readings[i]
print("Reading", reading)
i += 1
if reading > 20: # ...test at the end
break
Reading 12 Reading 18 Reading 25
Count-controlled loops: for
When the number of passes is known before the loop starts, a counter
controls it. The classic C-style for puts the three
parts of counting in one line: start value, condition, and step.
#include <stdio.h>
int main(void) {
for (int round = 1; round <= 3; round++) {
printf("Round %d\n", round);
}
return 0;
}
Round 1 Round 2 Round 3
Python has no C-style for; it counts by looping over
range(1, 4), which produces the numbers 1, 2 and 3. See
Loops.
Collection-controlled loops: for-each
Most loops in practice walk through a collection: every order in a
list, every line in a file. A for-each loop hands
you the items one by one, without a counter or an index, so there is
no way to get the bounds wrong. Python's for is always
a for-each loop; JavaScript has for … of, Java has
for (String name : names).
FOR EACH guest IN guests
PRINT "Welcome, " + guest
END FOR
const guests = ["Ada", "Grace", "Linus"];
for (const guest of guests) {
console.log(`Welcome, ${guest}`);
}
Welcome, Ada Welcome, Grace Welcome, Linus
Leaving a loop early: break and continue
Two statements change the flow inside a loop.
break leaves the loop immediately; execution continues
after it. continue skips the rest of the current pass
and goes on with the next one. Both exist, with the same meaning, in
C, Java, JavaScript and Python.
# -999 marks a sensor error, 999 marks the end of the data
temperatures = [18, -999, 21, 24, 999, 19]
for t in temperatures:
if t == 999:
break # stop reading altogether
if t == -999:
continue # skip this value, go on with the next
print(t)
18 21 24
Used sparingly, both make loops simpler. A loop with many exits,
though, becomes hard to reason about — the same problem that
goto had, on a smaller scale. More examples, including
Python's else clause on loops, are in
Loops.
Structured programming and goto
Early languages such as Fortran and BASIC relied on
GOTO: jump to a numbered line, from anywhere, to
anywhere. A loop was built by hand from a test and a jump back:
10 LET I = 1 20 IF I > 3 THEN GOTO 60 30 PRINT I 40 LET I = I + 1 50 GOTO 20 60 PRINT "DONE"
This works, but a reader has to follow the jumps to discover that
lines 20 to 50 form a loop. In a large program, jumps criss-cross
until nobody can tell which paths lead to a given line — "spaghetti
code". In 1968, Edsger Dijkstra published a short letter in
Communications of the ACM under the title "Go To
Statement Considered Harmful" (the title was chosen by the editor,
Niklaus Wirth). His argument: the quality of programmers decreases
with the density of goto statements in their code,
because unrestricted jumps make it nearly impossible to relate the
program text to what happens when it runs.
The answer was structured programming: building programs only from sequence, selection and iteration, plus subroutines, with each block having a single entry and a single exit. The same loop, structured:
FOR i ← 1 TO 3
PRINT i
END FOR
PRINT "DONE"
Structured code can be read from top to bottom, and each block can
be understood — and tested — on its own. By the 1980s it was the
normal way to program. Python and JavaScript have no
goto, and Java reserves the word but does not use it. C
still has it; its one widely accepted use is jumping to shared
clean-up code at the end of a function when an error occurs.
Nesting and readability
Control structures can be nested without limit, but every level adds
something the reader has to keep in mind. Code that drifts ever
further to the right is hard to follow: by the innermost line, which
conditions still apply? A common cure is the
guard clause: handle the special and invalid cases
first, each with an early return, so that the main case
is left at the bottom with no nesting at all.
def shipping_nested(weight_kg, country):
if weight_kg > 0:
if country in ("NL", "BE"):
if weight_kg <= 2:
return 4.95
else:
return 7.50
else:
return 14.00
else:
return None
def shipping_flat(weight_kg, country):
if weight_kg <= 0:
return None # guard: invalid weight
if country not in ("NL", "BE"):
return 14.00 # guard: international
if weight_kg <= 2:
return 4.95
return 7.50
for weight, country in [(1, "NL"), (5, "BE"), (1, "US"), (0, "NL")]:
print(weight, country, shipping_nested(weight, country), shipping_flat(weight, country))
1 NL 4.95 4.95 5 BE 7.5 7.5 1 US 14.0 14.0 0 NL None None
Both functions give the same results, but the flat version reads as a list of rules. Strict structured programming allowed only one exit per function; today, early returns at the top of a function are widely accepted because they reduce complexity rather than add to it. When nesting still grows deep, move the inner part into a function of its own — see Functions, Abstraction and Decomposition.