Control Flow — if / elif / else
How Python evaluates truthiness, every form of conditional logic, structural pattern matching, and real readability patterns.
if, elif, else — And Why Indentation Is Not Optional
Python uses indentation, not curly braces, to define which lines of code belong inside a conditional block. This is not a stylistic preference — it is the syntax. The standard, near-universal convention is 4 spaces per indentation level (never tabs, and never mixed tabs and spaces — Python will refuse to run code that mixes them within the same block).
age = 20
if age < 13:
print("Child")
elif age < 20:
print("Teenager")
elif age < 65:
print("Adult")
else:
print("Senior")
# Output: AdultPython evaluates the conditions top to bottom and runs the block under the first one that is true — every later condition is skipped entirely, even if it would also have been true. elif is Python's spelling of "else if" — there is no separate else if keyword pair, and you can chain as many elif blocks as you need. else is always optional; a program with only if and no else is completely valid, it simply does nothing when the condition is false.
IndentationError. Configure your editor to insert spaces (not a literal tab character) when you press Tab — VS Code with the Python extension does this correctly by default.Single-line if statements
Python permits writing a simple if body on the same line, without an indented block — legal, but generally discouraged for anything beyond the most trivial single statement, since it becomes hard to read and hard to extend if a second statement needs adding later.
if age >= 18: print("Adult")
# Preferred, even for one line — clearer, and trivially extensible:
if age >= 18:
print("Adult")What Python Actually Evaluates as True or False
An if condition does not need to be a literal True/False — Python evaluates any value for truthiness. Every value is considered truthy except a specific, well-defined set of falsy values.
False
None
0 # the integer zero
0.0 # the float zero
"" # an empty string
[] # an empty list
{} # an empty dict
() # an empty tuple
set() # an empty set
# Everything else is truthy — including "0" (a non-empty string!), [0], and -1items = []
if items:
print(f"You have {len(items)} items")
else:
print("Your list is empty")
# Equivalent to, but more idiomatic than:
if len(items) > 0:
...if items: rather than if len(items) > 0:, and if name: rather than if name != "":. This is not just shorter — it is the style every experienced Python reviewer expects, and linters like pylint will flag the more verbose form.How custom objects define their own truthiness
You will not need this until the Object-Oriented Python phase of this track, but it is worth knowing now that truthiness is not hardcoded for every type — a custom class can define its own truthiness rules by implementing a special method called __bool__. This is exactly how if items: works for a list: Python calls the list's internal truthiness logic, which reports "falsy" for an empty list and "truthy" for a non-empty one.
The Ternary Expression — if/else in a Single Line
Python's conditional expression (sometimes called a ternary) lets you choose between two values in a single expression, useful when assigning a value based on a condition without writing a full multi-line if/else block.
age = 20
status = "Adult" if age >= 18 else "Minor"
# Equivalent to the longer form:
if age >= 18:
status = "Adult"
else:
status = "Minor"The conditional expression is genuinely idiomatic when used inside another expression, not just for simple standalone assignment — for example, inside an f-string or as a function argument, where a full if/else block cannot syntactically appear at all.
count = 3
print(f"You have {count} item{'s' if count != 1 else ''}")
# "You have 3 items" — correctly pluralised in a single line
result = max(0, value if value > 0 else 0) # inside a function call argumentx = "A" if a else "B" if b else "C" technically works, but it is genuinely hard to read at a glance and is a common target of code review feedback. If you need more than one branch, write a full if/elif/else block instead — clarity beats brevity here.Guard Clauses — Avoiding the Arrow of Doom
Deeply nested if statements are one of the most common readability problems in beginner code — sometimes called the "arrow of doom" because the code visually drifts rightward with every nested level.
def process_order(order):
if order is not None:
if order.is_paid:
if order.items:
if order.shipping_address:
return "Ready to ship"
else:
return "Missing shipping address"
else:
return "No items in order"
else:
return "Payment required"
else:
return "No order provided"A guard clause restructures this by handling the failure conditions first and returning early, so the "happy path" is not nested inside four levels of indentation.
def process_order(order):
if order is None:
return "No order provided"
if not order.is_paid:
return "Payment required"
if not order.items:
return "No items in order"
if not order.shipping_address:
return "Missing shipping address"
return "Ready to ship"Combining conditions to reduce nesting
Not every nested if needs a guard-clause rewrite — sometimes the cleanest fix is simply combining conditions with and, which you already met in the Operators module.
# Nested — unnecessary, since both checks lead to the same single outcome
if age >= 18:
if has_id:
print("Entry allowed")
# Flat — identical behaviour, easier to read
if age >= 18 and has_id:
print("Entry allowed")match / case — Python's Modern Switch Statement
Introduced in Python 3.10, match/case gives Python a form of switch statement — but significantly more powerful, since it can match on structure and type, not just simple equality.
def describe_status(code):
match code:
case 200:
return "OK"
case 404:
return "Not Found"
case 500 | 502 | 503:
return "Server Error"
case _:
return "Unknown status"
describe_status(404) # "Not Found"
describe_status(502) # "Server Error" — the | matches multiple values in one case
describe_status(999) # "Unknown status" — the _ is a wildcard, matching anythingStructural matching — where match/case genuinely goes beyond a switch statement
The real power of match is matching against the shape of data, not just a single value — genuinely something a traditional switch statement cannot do.
def handle_event(event):
match event:
case {"type": "click", "x": x, "y": y}:
return f"Click at ({x}, {y})"
case {"type": "keypress", "key": key}:
return f"Key pressed: {key}"
case {"type": type_name}:
return f"Unhandled event type: {type_name}"
case _:
return "Not a recognised event"
handle_event({"type": "click", "x": 10, "y": 20})
# "Click at (10, 20)" — x and y are extracted from the dict automaticallydef classify_point(point):
match point:
case (0, 0):
return "Origin"
case (x, 0):
return f"On the x-axis at {x}"
case (0, y):
return f"On the y-axis at {y}"
case (x, y) if x == y:
return "On the diagonal"
case (x, y):
return f"Point at ({x}, {y})"
classify_point((3, 3)) # "On the diagonal" — the "if" after a case is a guard conditionif/elif/else far more often than match/case in everyday code — it genuinely shines for a specific case: matching against several discrete, known values, or unpacking structured data like a dict or tuple shape, which you will use for real once you reach the Object-Oriented Python and Advanced Python phases of this track, particularly when working with API responses and parsed data.assert — Sanity-Checking Assumptions During Development
assert checks that a condition is true, and raises an AssertionError immediately if it is not. It is a control-flow-adjacent tool used to catch programming mistakes early, not to validate user input or handle expected failure cases (that is what the Exception Handling module, later in this track, is for).
def calculate_discount(price, percent):
assert 0 <= percent <= 100, f"Invalid discount percent: {percent}"
return price * (1 - percent / 100)
calculate_discount(100, 20) # 80.0 — fine
calculate_discount(100, 150) # AssertionError: Invalid discount percent: 150assert statement entirely (the -O flag) — meaning code that relies on assert to enforce a real business rule can silently stop checking anything at all in that mode. Use assert for catching your own programming mistakes during development (an "impossible" state that should never happen if the code is correct) — never for validating data that comes from users, files, or external APIs, which will be covered properly with exceptions later in this track.pass — The Explicit "Do Nothing" Statement
Python's indentation-based syntax requires every block to have at least one statement inside it — an if, for, def, or class with a genuinely empty body is a SyntaxError. pass is a statement that does precisely nothing, existing solely to satisfy this requirement.
def function_to_implement_later():
pass # a stub — the function exists and is callable, but does nothing yet
if condition:
pass # deliberately no action for this case
else:
do_something()This comes up constantly during early development — sketching out the shape of a program (which functions and classes will exist) before filling in their real logic, and needing something syntactically valid to write in the meantime.
A Code Review at a Boston Logistics Startup
A new engineer submits a function that determines a shipment's status message based on its delivery state — a genuinely reasonable first attempt at the logic.
def get_status_message(shipment):
if shipment is not None:
if shipment.is_delivered == True:
if shipment.signature_received == True:
message = "Delivered and signed for"
else:
if shipment.left_at_door == True:
message = "Delivered — left at door"
else:
message = "Delivered"
else:
message = "In transit"
else:
message = "Shipment not found"
return messageWhat the reviewer flags
Three separate issues, each directly traceable to earlier parts of this module: the deep nesting is a textbook case for guard clauses (Part 04); every == True comparison should simply be the condition itself, since shipment.is_delivered is already a boolean (a truthiness-check idiom, Part 02); and the logic can be flattened entirely into ordered elif branches, since these are genuinely mutually exclusive outcomes, not independent nested decisions.
def get_status_message(shipment):
if shipment is None:
return "Shipment not found"
if not shipment.is_delivered:
return "In transit"
if shipment.signature_received:
return "Delivered and signed for"
if shipment.left_at_door:
return "Delivered — left at door"
return "Delivered"Same behaviour, eight lines shorter, and every branch is readable in isolation without mentally tracking four levels of nested indentation. This exact transformation — flatten nested conditionals, drop redundant == True comparisons, use guard clauses for early exits — is quite possibly the single most common category of feedback given in real Python code review for engineers early in their career.
Four Misconceptions About Control Flow
5 Interview Questions — With Complete Answers
Control Flow Mistakes Beginners Make Constantly
Errors You Will Hit With Control Flow — And Exactly Why
🎯 Key Takeaways
- ✓Python uses indentation (4 spaces, by convention) to define blocks — not braces. Every if/elif/else/for/while/def/class line ends with a colon.
- ✓Only the first matching elif/else branch runs — Python stops checking as soon as one condition is true.
- ✓Every value has a truthiness. Falsy values are exactly: False, None, 0, 0.0, "", [], {}, (), and set(). Everything else is truthy.
- ✓Idiomatic Python favours truthiness checks (if items:) over explicit comparisons (if len(items) > 0: or if x == True:).
- ✓Guard clauses (handling failure cases first with early returns) are the standard professional pattern for avoiding deeply nested conditionals — one of the most common real code review requests.
- ✓match/case (Python 3.10+) is a more powerful switch-statement alternative, capable of structurally matching the shape of dicts and tuples, not just comparing single values.
- ✓assert checks your own assumptions during development and can be globally stripped out with the -O flag — never use it to validate external or user-provided data.
- ✓pass is a no-op statement used as a placeholder wherever Python's syntax requires a non-empty indented block.
What comes next
Module 06 (Loops) and the rest of the Python Foundations phase are being written now and will go live soon. In the meantime, browse the full 46-module curriculum below.
← Back to the Python trackDiscussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.