Conditional Statements #
Conditional statements are the mechanism that lets a program take different execution paths based on certain conditions. In Python, there are two main constructs for this: if/elif/else, which has been around since the beginning, and match/case, introduced in Python 3.10 as a form of structural pattern matching that’s far richer than a plain switch in other languages. Just as important as knowing the syntax is understanding how to structure conditions so code stays readable — because deep, nested branching is one of the leading causes of hard-to-maintain code.
if, elif, and else
#
The basic form of conditional selection in Python. The colon : ends the condition line, and the code block below it is indented 4 spaces.
# Basic structure
score = 78
if score >= 90:
print("A")
elif score >= 80:
print("B")
elif score >= 70:
print("C")
elif score >= 60:
print("D")
else:
print("E")
# → C
To visualize how the Python interpreter evaluates if-elif-else branching sequentially and skips the remaining conditions as soon as one is satisfied, look at the flow diagram below:
flowchart TD
Start["Start Evaluation"] --> Cond1{"Condition 1 (if)?"}
Cond1 -->|True| Block1["Execute Block 1"]
Cond1 -->|False| Cond2{"Condition 2 (elif)?"}
Cond2 -->|True| Block2["Execute Block 2"]
Cond2 -->|False| Cond3{"Condition 3 (elif)?"}
Cond3 -->|True| Block3["Execute Block 3"]
Cond3 -->|False| BlockElse["Execute else Block"]
Block1 --> End["Conditional Selection Done"]
Block2 --> End
Block3 --> End
BlockElse --> EndAfter one block finishes executing, program control jumps straight to the end of the conditional selection without evaluating the remaining branches.
Some important things about if/elif/else behavior:
# elif is evaluated ONLY if all previous conditions are False
# Once a condition is True, the rest are skipped entirely
x = 15
if x > 10:
print("more than 10") # ← this runs
elif x > 5:
print("more than 5") # ← NOT evaluated even though 15 > 5
else:
print("5 or less") # ← this doesn't run either
# else is optional
age = 20
if age >= 18:
print("allowed in")
# no else needed if there's no action for the other case
Truthy and Falsy Conditions in if
#
Python evaluates any expression as a condition — it doesn’t have to be an explicit boolean. This enables more concise idioms but needs to be understood well.
# Values considered False (falsy):
# False, 0, 0.0, "", [], {}, set(), tuple(), None
# Values considered True (truthy):
# everything else
name = ""
data = []
user = None
# ANTI-PATTERN: unnecessary explicit comparisons
if name == "":
print("name is empty")
if data == []:
print("list is empty")
if user == None: # should be 'is None'
print("no user")
# CORRECT: leverage truthy/falsy directly
if not name:
print("name is empty")
if not data:
print("list is empty")
if user is None: # None always with 'is'
print("no user")
# A case that often surprises — the number 0 is falsy
stock = 0
if not stock:
print("out of stock") # → out of stock (logically correct)
# But be careful if 0 is a valid value distinct from "absent"
score = 0
if not score:
print("score not filled") # ← WRONG! 0 is a valid score
# CORRECT: distinguish between "the value 0" and "no value"
score = 0
if score is None:
print("score not filled")
else:
print(f"score: {score}") # → score: 0
Nested if and How to Avoid It
#
Nested conditions often arise naturally, but going too deep makes code hard to read and maintain. There are several techniques to flatten them.
# ANTI-PATTERN: deep nested if — the "pyramid of doom"
def process_transaction(user, amount):
if user is not None:
if user.active:
if amount > 0:
if amount <= user.balance:
if user.daily_limit >= amount:
do_transfer(user, amount)
return True
else:
return False
else:
return False
else:
return False
else:
return False
else:
return False
Technique 1: Guard Clauses (Early Return) #
Flip the conditions and return early for the cases that don’t pass. The “happy path” code stays at the leftmost indentation.
The Guard Clause concept aims to clean up code structure by eliminating deep nested branching. Compare the control-flow diagrams below:
flowchart TD
subgraph Nested ["Nested Model (Nested If)"]
n_start["Start"] --> n_cond1{"Condition 1?"}
n_cond1 -->|True| n_cond2{"Condition 2?"}
n_cond2 -->|True| n_cond3{"Condition 3?"}
n_cond3 -->|True| n_happy["Happy Path (Execution)"]
n_cond1 -->|False| n_err["Return False"]
n_cond2 -->|False| n_err
n_cond3 -->|False| n_err
end
subgraph Guard ["Guard Clause Model (Early Return)"]
g_start["Start"] --> g_cond1{"Fails 1?"}
g_cond1 -->|True| g_err1["Return False"]
g_cond1 -->|False| g_cond2{"Fails 2?"}
g_cond2 -->|True| g_err2["Return False"]
g_cond2 -->|False| g_happy["Happy Path (Execution)"]
endWith Guard Clauses, every failure condition is handled instantly at the top of the function (an emergency exit door), leaving the end of the function for clean main logic without excessive indentation.
# CORRECT: a guard clause flips each condition and returns early
def process_transaction(user, amount):
if user is None:
return False
if not user.active:
return False
if amount <= 0:
return False
if amount > user.balance:
return False
if amount > user.daily_limit:
return False
# happy path — all validations passed
do_transfer(user, amount)
return True
Technique 2: Combine Conditions with and
#
# ANTI-PATTERN: nested ifs for conditions that actually belong together
def check_access(user, feature):
if user.logged_in:
if user.active:
if feature in user.permissions:
return True
return False
# CORRECT: combine with and
def check_access(user, feature):
if user.logged_in and user.active and feature in user.permissions:
return True
return False
# Or even more concise
def check_access(user, feature):
return user.logged_in and user.active and feature in user.permissions
Technique 3: Extract into a Separate Function #
# ANTI-PATTERN: long validation logic inside an if
def save_product(data):
if (data.get("name") and
len(data["name"]) >= 3 and
data.get("price") and
data["price"] > 0 and
data.get("stock") is not None and
data["stock"] >= 0):
# save to the database
db.save(data)
# CORRECT: extract validation into its own function
def product_is_valid(data):
"""Return True if the product data meets all requirements."""
if not data.get("name") or len(data["name"]) < 3:
return False
if not data.get("price") or data["price"] <= 0:
return False
if data.get("stock") is None or data["stock"] < 0:
return False
return True
def save_product(data):
if product_is_valid(data):
db.save(data)
Conditional Expressions (Ternary) #
Python provides a one-line conditional expression — often called the ternary expression. Useful for simple assignments, but don’t force it for complex conditions.
# Syntax: value_if_true if condition else value_if_false
x = 10
label = "positive" if x > 0 else "zero or negative"
print(label) # → positive
# Common uses
age = 20
status = "adult" if age >= 18 else "minor"
discount = 0.10 if total_spending >= 500_000 else 0
final_price = price * (1 - discount)
# Can be used directly inside other expressions
print(f"You {'may' if age >= 18 else 'may not'} enter.")
# ANTI-PATTERN: nested ternaries — very hard to read
result = "A" if score >= 90 else "B" if score >= 80 else "C" if score >= 70 else "D"
# CORRECT: use regular if/elif/else for multi-branch conditions
if score >= 90:
result = "A"
elif score >= 80:
result = "B"
elif score >= 70:
result = "C"
else:
result = "D"
Conditions with Collections #
Some condition patterns are commonly used with data collections:
# Check whether a value exists in a list/set/dict
admin_roles = {"admin", "superadmin", "moderator"}
user_role = "admin"
# ANTI-PATTERN: a long chain of ors
if user_role == "admin" or user_role == "superadmin" or user_role == "moderator":
grant_panel_access()
# CORRECT: use 'in' with a set
if user_role in admin_roles:
grant_panel_access()
# Conditions based on collection length
message_list = []
# ANTI-PATTERN
if len(message_list) == 0:
print("no messages")
if len(message_list) > 0:
show_messages()
# CORRECT: leverage collection truthiness
if not message_list:
print("no messages")
if message_list:
show_messages()
# Dispatch table — an alternative to long if/elif using a dict
def action_add(): print("adding item")
def action_delete(): print("deleting item")
def action_update(): print("updating item")
def action_view(): print("viewing item")
# ANTI-PATTERN: long if/elif for action routing
def run_action(command):
if command == "add":
action_add()
elif command == "delete":
action_delete()
elif command == "update":
action_update()
elif command == "view":
action_view()
else:
print(f"unknown command: {command}")
# CORRECT: dispatch table with a dict
ACTION_MAP = {
"add": action_add,
"delete": action_delete,
"update": action_update,
"view": action_view,
}
def run_action(command):
action = ACTION_MAP.get(command)
if action:
action()
else:
print(f"unknown command: {command}")
match/case — Structural Pattern Matching
#
Introduced in Python 3.10, match/case is not just a switch. It can match the structure of data — literal values, tuples, lists, dicts, object types — while extracting values from inside them at the same time.
Matching Literal Values #
def day_name(number):
match number:
case 1:
return "Monday"
case 2:
return "Tuesday"
case 3:
return "Wednesday"
case 4:
return "Thursday"
case 5:
return "Friday"
case 6:
return "Saturday"
case 7:
return "Sunday"
case _: # wildcard — matches anything (like default)
return "Invalid"
print(day_name(3)) # → Wednesday
print(day_name(9)) # → Invalid
Matching with | (OR Pattern)
#
def day_category(number):
match number:
case 6 | 7:
return "Weekend"
case 1 | 2 | 3 | 4 | 5:
return "Weekday"
case _:
return "Invalid"
print(day_category(6)) # → Weekend
print(day_category(3)) # → Weekday
Matching Sequences (Tuple/List) #
This is the main advantage of match/case — it can match structure and extract values into variables at the same time:
def describe_point(point):
match point:
case (0, 0):
return "Origin"
case (0, y): # x=0, y free → captured into variable y
return f"On the Y axis: y={y}"
case (x, 0): # y=0, x free → captured into variable x
return f"On the X axis: x={x}"
case (x, y) if x == y: # guard condition
return f"On the diagonal line: ({x}, {y})"
case (x, y): # any other point
return f"Regular point: ({x}, {y})"
case _:
return "Not a 2D coordinate"
print(describe_point((0, 0))) # → Origin
print(describe_point((0, 5))) # → On the Y axis: y=5
print(describe_point((3, 0))) # → On the X axis: x=3
print(describe_point((4, 4))) # → On the diagonal line: (4, 4)
print(describe_point((3, 7))) # → Regular point: (3, 7)
Matching Dictionaries (Mapping Pattern) #
def process_event(event):
match event:
case {"type": "click", "button": "left", "x": x, "y": y}:
print(f"Left click at ({x}, {y})")
case {"type": "click", "button": "right", "x": x, "y": y}:
print(f"Right click at ({x}, {y})")
case {"type": "keyboard", "key": key} if key.startswith("F"):
print(f"Function key: {key}")
case {"type": "keyboard", "key": key}:
print(f"Key: {key}")
case _:
print("Unknown event")
process_event({"type": "click", "button": "left", "x": 100, "y": 200})
# → Left click at (100, 200)
process_event({"type": "keyboard", "key": "F5"})
# → Function key: F5
The mapping pattern inmatch/caseis partial — the dict being matched may have extra keys beyond those named in the pattern and still match. If you want an exact match, add**_to capture the remaining keys.
Matching Object Types (Class Pattern) #
from dataclasses import dataclass
@dataclass
class Circle:
radius: float
@dataclass
class Square:
side: float
@dataclass
class Triangle:
base: float
height: float
def compute_area(shape):
match shape:
case Circle(radius=r):
return 3.14159 * r ** 2
case Square(side=s):
return s ** 2
case Triangle(base=b, height=h):
return 0.5 * b * h
case _:
raise ValueError(f"Unknown shape: {shape}")
print(compute_area(Circle(7))) # → 153.938...
print(compute_area(Square(5))) # → 25.0
print(compute_area(Triangle(6, 4))) # → 12.0
When to Use match/case vs if/elif
#
Use if/elif when:
✓ conditions involve numeric comparisons (>, <, >=)
✓ conditions involve many different variables
✓ conditions combine easily with and/or
✓ you need compatibility with Python < 3.10
Use match/case when:
✓ matching data structures (tuples, dicts, objects)
✓ extracting values from a structure while matching
✓ the condition depends on the object type
✓ replacing a long if/elif comparing a single variable
Summary #
- Guard clauses (early returns) are the best way to avoid deep nested ifs — flip the condition and return early, leaving the happy path at the leftmost indentation.
- Avoid comparing with
== True,== False, or== []— just evaluate the expression directly, or usenotfor falsy cases.is None, not== None— always useis/is notto compare withNone.- A dispatch table (dict of functions) is cleaner than a long if/elif for routing based on string or enum values.
- Ternary expressions are great for simple two-branch conditions, but don’t nest them — use regular if/elif for more than two branches.
match/caseis not just a switch — it can match data structure and extract values into variables at the same time, available since Python 3.10.- Guard conditions in
match/caseuseifafter the pattern — enabling extra conditions beyond the structural match.- Combine related conditions with
andinstead of nested ifs — flatter and easier to read.