programming · level 2

Control Flow

Branches, loops, and the flow of execution.

100 XP

Control Flow

A program without control flow is just a list of statements. Control flow is how a program decides what to do next: branching on conditions, repeating work, choosing between options.

The two primitives

Almost every control-flow construct boils down to two ideas:

  1. Branching — pick one of several paths based on a condition (if, else, switch, match).
  2. Looping — repeat a path while a condition holds (while, for).

Everything else (recursion, generators, exceptions) is built on top.

If, elif, else

The bread-and-butter branch:

if user.is_admin:
    show_admin_panel()
elif user.is_logged_in:
    show_user_dashboard()
else:
    show_login_page()

Conditions are checked top-to-bottom. The first True branch runs and the rest are skipped — even if a later branch's condition is also true. Order matters: put the most specific case first.

Truthy and falsy

if some_value: doesn't require a boolean. Most languages have rules for what counts as "truthy" vs "falsy":

Language Falsy values
Python False, 0, 0.0, "", [], {}, None
JavaScript false, 0, "", null, undefined, NaN
Go false (only — no truthy coercion)
Rust false (only — same)

Go and Rust force you to write if list.len() > 0 instead of if list:. That verbosity is a deliberate guard against bugs like if user_count accidentally treating 0 as "no users".

Loops

A while loop runs as long as a condition is true:

while not done:
    next_chunk = fetch()
    process(next_chunk)
    done = next_chunk.is_last

A for loop is for iterating a known sequence:

for item in cart:
    total += item.price

In most languages for is the higher-level construct — you don't manage an index by hand. C-style for (int i = 0; i < n; i++) is rare in modern code; you write for i in range(n) or for x in array instead.

break and continue

Two escape hatches inside loops:

  • break — exit the loop immediately.
  • continue — skip to the next iteration.
for line in file:
    if line.startswith("#"):
        continue           # skip comments
    if line == "END":
        break              # stop reading
    process(line)

Use them sparingly. A loop body that's mostly continues is usually a loop body that should have been a filter.

switch / match

When you have many discrete cases, a switch (or modern match) is clearer than a chain of if/elif:

match status:
    case "ok": return 200
    case "not_found": return 404
    case "server_error": return 500
    case _: return 500          # default
switch (status) {
  case "ok": return 200;
  case "not_found": return 404;
  default: return 500;
}

JavaScript's switch requires break between cases or you'll fall through to the next one (a famously easy bug). Modern languages (Go's switch, Rust's match, Python's match) make each case implicitly break, which is the safer default.

Early return

Long nested conditions are hard to read. Prefer guard clauses that bail out early:

# Hard to read — happy path is at the bottom.
def charge(user, amount):
    if user is not None:
        if user.is_active:
            if amount > 0:
                # ... actual work ...
                return success
            else:
                return error("invalid amount")
        else:
            return error("inactive user")
    else:
        return error("no user")

# Easier — guard clauses up top, happy path runs flat.
def charge(user, amount):
    if user is None:
        return error("no user")
    if not user.is_active:
        return error("inactive user")
    if amount <= 0:
        return error("invalid amount")
    # ... actual work ...
    return success

Tip: don't repeat the condition

If you find yourself writing if x and ... elif x and ..., hoist the x check up:

# Repeated.
if user.is_admin and feature == "delete": ...
elif user.is_admin and feature == "edit": ...
elif user.is_admin and feature == "view": ...

# Cleaner.
if user.is_admin:
    if feature == "delete": ...
    elif feature == "edit": ...
    elif feature == "view": ...