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.

Flowcharts of the three control structures. Sequence: three steps one after another. Selection: a condition leads to one of two steps, which then join. Iteration: a condition leads to the body, which loops back to the condition, until the condition is false and the flow exits. Sequence Step 1 Step 2 Step 3 Selection cond? yes no Step A Step B Iteration cond? yes no Body
The three control structures as flowcharts. Each has a single entry at the top and a single exit at the bottom, so they can be nested inside each other.

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.