Control Flow
Branches, loops, and the flow of execution.
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:
- Branching — pick one of several paths based on a condition (
if,else,switch,match). - 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": ...