Python Basics — Nested Conditions
Nested Conditions
Nesting means placing one if statement inside another. It lets you check conditions step by step — like a decision tree.
Basic Nesting
age = 25
has_id = True
if age >= 18:
if has_id:
print("Entry allowed.")
else:
print("Please bring your ID.")
else:
print("Entry denied — must be 18+.")
Output:
Entry allowed.
Each level of nesting requires an additional 4-space indent.
When Nesting Is Appropriate
Nesting works well when decisions depend on each other:
is_member = True
cart_total = 150
if is_member:
if cart_total >= 100:
discount = 0.20
print(f"Member discount: {discount:.0%}")
else:
discount = 0.10
print(f"Member discount: {discount:.0%}")
else:
if cart_total >= 200:
discount = 0.05
print(f"Guest discount: {discount:.0%}")
else:
discount = 0
print("No discount available.")
Output:
Member discount: 20%
The Problem with Deep Nesting
Too much nesting hurts readability:
# Bad — deeply nested and hard to follow
def process_order(user, order):
if user is not None:
if user.is_active:
if order is not None:
if order.total > 0:
if user.balance >= order.total:
print("Order processed!")
else:
print("Insufficient balance.")
else:
print("Empty order.")
else:
print("No order provided.")
else:
print("Account inactive.")
else:
print("No user provided.")
This code forms an “arrow shape” — a sign of trouble.
Flattening Nested Conditions
Strategy 1: Combine with and
# Nested
if age >= 18:
if has_id:
print("Entry allowed.")
# Flattened — same result
if age >= 18 and has_id:
print("Entry allowed.")
Strategy 2: Guard Clauses (Early Return)
Instead of nesting deeper, return early for invalid cases:
# Flattened with guard clauses
def process_order(user, order):
if user is None:
print("No user provided.")
return
if not user.is_active:
print("Account inactive.")
return
if order is None:
print("No order provided.")
return
if order.total <= 0:
print("Empty order.")
return
if user.balance < order.total:
print("Insufficient balance.")
return
print("Order processed!")
This reads from top to bottom — each guard clause handles one failure case. By the time you reach the last line, all conditions are guaranteed to be met.
Strategy 3: Extract into Functions
def is_eligible_for_discount(member, total):
return member and total >= 100
def calculate_discount(member, total):
if is_eligible_for_discount(member, total):
return 0.20
elif member:
return 0.10
elif total >= 200:
return 0.05
return 0
discount = calculate_discount(True, 150)
print(f"Discount: {discount:.0%}")
Output:
Discount: 20%
Readability Guideline
A useful rule of thumb: if your code indents more than 3 levels, refactor.
| Nesting Level | Verdict |
|---|---|
| 1 level | Fine |
| 2 levels | Usually fine |
| 3 levels | Consider refactoring |
| 4+ levels | Definitely refactor |
Practical Example: ATM Withdrawal
def withdraw(balance, amount, pin_entered, daily_limit_remaining):
# Guard clauses — handle failures first
if not pin_entered:
return "Error: PIN not verified."
if amount <= 0:
return "Error: Invalid amount."
if amount > daily_limit_remaining:
return f"Error: Exceeds daily limit (${daily_limit_remaining} remaining)."
if amount > balance:
return "Error: Insufficient funds."
# Happy path — all checks passed
new_balance = balance - amount
return f"Dispensing ${amount}. New balance: ${new_balance}"
# Test
print(withdraw(1000, 200, True, 500))
print(withdraw(1000, 200, False, 500))
print(withdraw(100, 200, True, 500))
Output:
Dispensing $200. New balance: $800
Error: PIN not verified.
Error: Insufficient funds.
Summary
- Nesting places
ifblocks inside otherifblocks - It’s useful when decisions genuinely depend on each other
- Deep nesting (3+ levels) hurts readability
- Flatten with
and/or, guard clauses, or helper functions - Guard clauses (early returns) are the most common refactoring pattern
Next up: Ternary Operator →