Python Control Flow

Master Python branching: if/elif/else, the match statement, ternary expressions, truthiness rules, and short-circuit patterns.

Beginner 10 min read 9 examples

if / elif / else

Python uses indentation instead of braces to define blocks. The colon (:) at the end of a condition starts the block.

Python
# Basic if/else
age = 20

if age >= 18:
    print("Adult")
else:
    print("Minor")

# if/elif/else chain
score = 85

if score >= 90:
    grade = "A"
elif score >= 80:
    grade = "B"
elif score >= 70:
    grade = "C"
elif score >= 60:
    grade = "D"
else:
    grade = "F"

print(f"Grade: {grade}")    # Grade: B

# Nested if
logged_in = True
is_admin  = False

if logged_in:
    if is_admin:
        print("Admin dashboard")
    else:
        print("User dashboard")
else:
    print("Please log in")

# Chained comparisons
x = 15
if 10 < x < 20:         # Python allows this - very readable
    print("x is between 10 and 20")

# Multiple conditions
username = "alice"
password = "secret"

if username == "alice" and password == "secret":
    print("Welcome, Alice!")
elif username == "alice" or username == "admin":
    print("Known user, wrong password")
else:
    print("Unknown user")

# One-liner if (only for very simple cases)
x = 5
if x > 0: print("positive")    # valid but not recommended for complex code

Truthiness

Python evaluates any object in a boolean context. Understanding what is falsy lets you write idiomatic, concise conditionals.

Python
# Falsy values - evaluate to False in a boolean context
falsy_values = [
    False, None,
    0, 0.0, 0j,                     # numeric zeros
    "", b"", [],  (), {}, set(),    # empty containers
]

for v in falsy_values:
    print(f"{repr(v):<12} -> {bool(v)}")

# Everything else is truthy
truthy_values = [True, 1, -1, 0.001, "a", " ", [0], {"k": None}]
for v in truthy_values:
    print(f"{repr(v):<15} -> {bool(v)}")

# Idiomatic Python conditionals
items = []
if not items:           # instead of: if len(items) == 0:
    print("List is empty")

name = "Alice"
if name:                # instead of: if name != "":
    print(f"Hello, {name}!")

config = {"debug": False}
if config:              # dict is truthy even if values are falsy
    print("Config loaded")

value = None
if value is None:       # use 'is None', not truthiness, for None checks
    print("No value")

# Default values using 'or'
user_input = ""
display_name = user_input or "Anonymous"   # "Anonymous" (input is falsy)
print(display_name)

count = 0
safe_count = count or 1     # 1 (count is 0, falsy)
print(safe_count)

# WARNING: 'or' default fails if 0 is a valid value
# Use explicit None check instead
def get_count():
    return 0

result = get_count()
value = result if result is not None else 1   # correct: 0 stays 0
# NOT: value = result or 1  (would replace 0 with 1 - wrong!)
Use is None, not truthiness, for optional values

When a variable can legitimately be 0, "", or [], do not test truthiness to detect "no value" - test is None explicitly. Truthiness checks are safe for "does this collection have items?" but unreliable for "was a value provided?"

Ternary Expression

Python's ternary is an expression (returns a value), not a statement. The syntax is: value_if_true if condition else value_if_false.

Python
age = 20

# Ternary expression
label = "adult" if age >= 18 else "minor"
print(label)    # adult

# In a print statement
print("yes" if age > 0 else "no")

# Inline assignment
score = 75
result = "pass" if score >= 60 else "fail"

# Nested ternary - avoid for readability
grade = "A" if score >= 90 else "B" if score >= 80 else "C" if score >= 70 else "D" if score >= 60 else "F"
# Hard to read - use if/elif/else instead

# Good use: simple, obvious conditions
items = [1, 2, 3]
msg = f"Found {len(items)} item{'s' if len(items) != 1 else ''}"
print(msg)  # Found 3 items

# In a list comprehension
nums = [-3, -1, 0, 2, 5]
abs_vals = [x if x >= 0 else -x for x in nums]
print(abs_vals)     # [3, 1, 0, 2, 5]

# As a function argument
name = None
print(f"Hello, {name or 'stranger'}!")   # Hello, stranger!

match-case (Python 3.10+)

The match statement is Python's structural pattern matching. It is more powerful than a traditional switch - it can match values, types, structures, and use guards.

Python
# Basic value matching (like switch)
command = "quit"

match command:
    case "quit":
        print("Quitting...")
    case "help":
        print("Available commands: quit, help, run")
    case "run":
        print("Running...")
    case _:                 # wildcard - matches anything (like default)
        print(f"Unknown command: {command}")

# Match with multiple values (| means OR)
status_code = 404

match status_code:
    case 200 | 201 | 204:
        print("Success")
    case 301 | 302:
        print("Redirect")
    case 400:
        print("Bad Request")
    case 404:
        print("Not Found")
    case 500 | 503:
        print("Server Error")
    case _:
        print(f"Unknown status: {status_code}")

# Match with guard (if condition)
x = 15
match x:
    case n if n < 0:
        print("negative")
    case 0:
        print("zero")
    case n if n < 10:
        print("small positive")
    case n if n < 100:
        print(f"medium positive: {n}")   # n is bound here
    case _:
        print("large")

# Structural matching - match data structure shapes
point = (0, 5)

match point:
    case (0, 0):
        print("Origin")
    case (x, 0):
        print(f"On x-axis at {x}")
    case (0, y):
        print(f"On y-axis at {y}")
    case (x, y):
        print(f"Point at ({x}, {y})")

# Match dictionaries
event = {"type": "click", "button": "left", "x": 100, "y": 200}

match event:
    case {"type": "click", "button": "left"}:
        print("Left click")
    case {"type": "click", "button": "right"}:
        print("Right click")
    case {"type": "keydown", "key": key}:
        print(f"Key pressed: {key}")
    case _:
        print("Other event")

# Match with class patterns
from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

p = Point(0, 5)

match p:
    case Point(x=0, y=0):
        print("Origin")
    case Point(x=0, y=y):
        print(f"On y-axis at y={y}")
    case Point(x=x, y=0):
        print(f"On x-axis at x={x}")
    case Point(x=x, y=y):
        print(f"Point({x}, {y})")

pass and ...

Python requires at least one statement in every block. Use pass or ... as no-op placeholders.

Python
# pass - traditional empty block placeholder
if condition := True:
    pass    # TODO: implement later

# Pass in empty function
def placeholder():
    pass    # function that does nothing (yet)

# Pass in empty class
class EmptyClass:
    pass

# ... (Ellipsis) - increasingly common alternative
def not_implemented_yet():
    ...

class AbstractBase:
    def process(self, data):
        ...     # subclasses must implement this

# Ellipsis is commonly used in type stubs and Protocol
from typing import Protocol

class Readable(Protocol):
    def read(self, size: int = -1) -> bytes: ...
    def close(self) -> None: ...

# pass vs ... - functionally identical for placeholders
# Use pass for: intentionally empty loops/branches
for _ in range(5):
    pass    # consuming the iterator without doing anything

# Use ... for: "to be implemented", abstract methods, type annotations

Short-Circuit Patterns

Python's logical operators and and or short-circuit - they stop evaluating as soon as the result is determined.

Python
# 'and' stops at first falsy, 'or' stops at first truthy

# Safe attribute/key access
user = {"name": "Alice", "address": None}

# Without short-circuit - might fail
# city = user["address"]["city"]    # AttributeError: 'NoneType' has no ...

# With 'and' - safe chaining
city = user.get("address") and user["address"].get("city")
print(city)     # None (address is None, stops there)

# Default values
settings = {}
timeout = settings.get("timeout") or 30    # 30 if not set
retries = settings.get("retries") or 3     # 3 if not set

# Call function only if object exists
def send_email(user):
    print(f"Sending to {user}")

user = None
user and send_email(user)   # send_email never called

user = "alice@example.com"
user and send_email(user)   # sends email

# Guard pattern - early return
def process(data):
    if not data:
        return None     # early exit for empty input
    if not isinstance(data, list):
        return None
    return [x * 2 for x in data]

# Dict dispatch instead of long if/elif chains
def add(a, b): return a + b
def sub(a, b): return a - b
def mul(a, b): return a * b

operations = {"+": add, "-": sub, "*": mul}

op = "+"
if op in operations:
    result = operations[op](10, 5)
    print(result)   # 15

Frequently Asked Questions

Python 3.10 introduced the match statement, which is more powerful than a traditional switch - it supports structural pattern matching, destructuring, guards, and wildcard patterns. For Python 3.9 and earlier, use if/elif/else chains, or a dict mapping values to callables for dispatch patterns.

The falsy values are: False, None, numeric zero (0, 0.0, 0j), empty sequences ("", [], (), b""), empty collections ({}, set()), and objects whose __bool__() or __len__() returns False/0. Everything else is truthy. This means you can write if my_list: instead of if len(my_list) > 0:.

Python uses an expression form: value_if_true if condition else value_if_false. Example: label = "adult" if age >= 18 else "minor". Unlike C's ? :, the condition goes in the middle. Keep ternaries simple - if the expression needs more than one line to read, use a regular if/else block for clarity.

Both are valid as empty body placeholders in functions, classes, and branches. pass is the traditional statement for empty blocks. ... (Ellipsis literal) is increasingly used in type stubs, abstract method bodies, and as a "not yet implemented" marker - it reads as "to be filled in" rather than "intentionally empty." Either works; use pass for branches and ... when a value placeholder is more natural.