Python · SQL · Web Dev · Java · AI/ML tracks launching soon — your one platform for all of IT
Beginner+150 XP

Control Flow — if / elif / else

How Python evaluates truthiness, every form of conditional logic, structural pattern matching, and real readability patterns.

55 min August 2026
// Part 01 — The Basics

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).

Basic if / elif / else
age = 20

if age < 13:
    print("Child")
elif age < 20:
    print("Teenager")
elif age < 65:
    print("Adult")
else:
    print("Senior")

# Output: Adult

Python 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.

⚠️ Important
Indentation must be consistent within a block. Mixing 2 spaces on one line and 4 on the next inside the same block raises an 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.

Legal, but not recommended for real code
if age >= 18: print("Adult")

# Preferred, even for one line — clearer, and trivially extensible:
if age >= 18:
    print("Adult")
// Part 02 — Truthy and Falsy Values

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.

The complete list of falsy values in Python
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 -1
This is why you can write conditions like this
items = []

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:
    ...
🎯 Pro Tip
Idiomatic Python favours truthiness checks over explicit comparisons. Write 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.

// Part 03 — Conditional Expressions

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.

Conditional expression syntax
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.

Where conditional expressions genuinely earn their place
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 argument
⚠️ Important
Do not chain more than one conditional expression on a single line. x = "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.
// Part 04 — Nested Conditions and Readability

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.

Deeply nested — hard to follow
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.

Guard clauses — flat and easy to follow
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"
🎯 Pro Tip
This is not just a stylistic preference — guard clauses are the standard pattern used throughout professional Python codebases, and you will see this exact restructuring requested constantly in real code reviews. Handle the exceptional/failure cases first and exit early; keep the main logic unindented and easy to scan.

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.

Combining conditions instead of nesting
# 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")
// Part 05 — Structural Pattern Matching

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.

Basic match/case — matching literal values
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 anything

Structural 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.

Matching the structure of a dict — a shape a switch statement cannot express
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 automatically
Matching the structure of a tuple, with a guard condition
def 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 condition
💡 Note
You will use plain if/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.
// Part 06 — assert

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).

assert in practice
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: 150
⚠️ Important
assert is not a substitute for proper error handling. Python can be run with optimisations that strip out every assert 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.
// Part 07 — pass

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.

pass as a placeholder
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.

// Part 08 — Real World
💼 What This Looks Like at Work

A Code Review at a Boston Logistics Startup

Scenario — Logistics startup, Boston · Pull request review

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.

The original submission
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 message

What 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.

The revised version
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.

// Part 09 — Misconceptions

Four Misconceptions About Control Flow

✕ ""match/case is just Python's version of switch from other languages""
It can do everything a switch statement does, but its real strength is structural matching — pulling apart the shape of a dict or tuple as part of the match itself, something a traditional switch statement has no equivalent for.
✕ ""assert is a good way to validate user input""
assert statements can be globally stripped out when Python runs with the -O optimisation flag, meaning any validation logic living inside an assert can silently vanish. Use assert only for catching your own programming mistakes during development; use proper exception handling (covered later in this track) for anything involving real user or external data.
✕ ""Nested if statements are just how you express complex logic""
Deep nesting is very often a readability problem with a straightforward fix — guard clauses, combining conditions with and, or flattening mutually-exclusive branches into if/elif/else, exactly as shown in the code review example above.
✕ ""if x == True: is clearer than if x: because it's more explicit""
It is considered less idiomatic, not more explicit — x is already a boolean, so comparing it to True adds a redundant step for the reader, and every Python style guide and linter flags it. if x: is the expected, idiomatic form.
// Part 10 — Interview Prep

5 Interview Questions — With Complete Answers

What is a guard clause, and why is it preferred over deeply nested conditionals?
A guard clause handles failure/exceptional conditions first with an early return, rather than nesting the "happy path" logic inside multiple levels of if statements. It keeps the main logic at a single indentation level, making it far easier to read and reason about — a deeply nested structure requires holding every enclosing condition in your head to understand any inner line, while guard clauses let each check be read and dismissed independently.
List the values that Python considers falsy.
False, None, 0, 0.0, the empty string "", and empty collections: [], {}, (), and set(). Every other value — including "0" as a string, and any non-empty collection — is truthy.
What does match/case offer beyond what a chain of if/elif/else can do?
Structural pattern matching — the ability to match against the SHAPE of data, not just a single value, extracting variables from a dict or tuple as part of the match itself (e.g. case {"type": "click", "x": x, "y": y}:). A chain of if/elif/else can express equivalent logic, but requires manually accessing and validating the structure inside each branch rather than expressing it declaratively in the pattern itself.
Why is assert not a safe way to validate data from an external source, like user input or an API response?
Python can run with the -O (optimize) flag, which strips out every assert statement in the program entirely. Any check written as an assert can therefore silently disappear depending on how the program is run — which is fine for catching your own logic errors during development, but unacceptable for validating data whose correctness the program actually depends on. Real input validation should use explicit if checks and raised exceptions instead.
What is the purpose of the pass statement?
It is a no-op — a statement that does nothing, used to satisfy Python's requirement that every indented block contain at least one statement. It is most commonly used as a placeholder in a function, class, or conditional branch that has not been implemented yet, or where a branch is intentionally meant to do nothing.
// Common Mistakes

Control Flow Mistakes Beginners Make Constantly

Forgetting the colon at the end of an if/elif/else line
if age >= 18 without a trailing colon raises a SyntaxError. The colon is what tells Python an indented block follows — it is required on every if, elif, else, for, while, def, and class line.
Using = instead of == inside a condition
if age = 18: is a SyntaxError in Python (unlike some other languages, where accidental assignment inside a condition silently compiles and causes a logic bug). Python protects you here — but it is still worth typing == deliberately rather than relying on the error message to catch it.
Writing if x == True: or if x == False:
This works but is not idiomatic and is flagged by every Python linter. Write if x: or if not x: instead — as shown in the code review example, this is one of the most common pieces of real-world feedback given to engineers early in their careers.
Not realising elif conditions are only checked if all prior ones were False
A common bug: writing several independent if statements when elif was intended, causing multiple blocks to run when only one should. If the conditions are meant to be mutually exclusive alternatives, use elif — not a sequence of separate if statements.
Using assert to validate data from users or external sources
As covered in Part 06, assert statements can be stripped out entirely when Python runs with the -O flag. Never rely on assert for anything the correctness of your program actually depends on in production.
// Error Library

Errors You Will Hit With Control Flow — And Exactly Why

IndentationError: expected an indented block after 'if' statement on line 3
Cause: An if, elif, else, for, while, def, or class line ends with a colon but the following line is not indented — Python requires at least one indented statement to follow.
Fix: Indent the line(s) that should belong to that block. If the block is genuinely meant to do nothing, use "pass" as a placeholder rather than leaving it empty.
IndentationError: unindent does not match any outer indentation level
Cause: Mixed use of tabs and spaces, or an inconsistent number of spaces, within the same logical block — Python cannot determine which enclosing block the line is meant to belong to.
Fix: Configure your editor to insert spaces (not tab characters) for indentation, and use "Convert Indentation to Spaces" if a file already has mixed tabs/spaces. VS Code with the Python extension handles this correctly by default.
AssertionError
Cause: An assert statement's condition evaluated to False. By design, this is meant to happen only when your own code reaches a state you believed was impossible.
Fix: Read the assertion message (if one was provided) and the surrounding logic — this signals a genuine bug in your program's assumptions, not something to silence or catch. Fix the underlying logic error rather than removing the assertion.
SyntaxError: invalid syntax (on a match/case block)
Cause: match/case requires Python 3.10 or newer. Running match/case syntax on an older Python version produces a SyntaxError, since the interpreter does not recognise the keywords at all.
Fix: Confirm your Python version with python3 --version. If you are on an older version, either upgrade or rewrite the logic as if/elif/else, which works on every Python 3 version.

🎯 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 track
Share

Discussion

0

Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.

Continue with GitHub
Loading...