if / elif / else
The if/elif/else statement is Python's core branching construct — indentation-defined, condition-driven, and built entirely around truthiness rather than explicit boolean coercion.
if, elif, and else let a program choose which block of code to run based on the truthiness of one or more conditions. Unlike many languages, Python has no switch statement in the classic sense (structural pattern matching's match/case, covered separately, fills some of that role) — if/elif/else chains are the everyday tool for conditional logic, and Python enforces their structure through indentation rather than braces.
Basic Syntax and Indentation
An if statement begins with if, a condition expression, and a colon; the indented block beneath it runs only when the condition is truthy. elif (short for 'else if') introduces additional conditions checked only if all prior conditions were falsy; else catches everything else and takes no condition of its own.
Indentation is not a style preference in Python — it is the actual syntax that delimits blocks. Standard style uses 4 spaces per indent level, and mixing tabs and spaces in the same block raises a TabError.
temperature = 15
if temperature > 30:
print('hot')
elif temperature > 15:
print('warm')
elif temperature > 0:
print('cool')
else:
print('freezing')
# prints 'cool'How Conditions Are Evaluated: Truthiness
The condition after if/elif is not required to be a bool. Python evaluates it for truthiness: any object can be tested for truth, and Python decides via __bool__ (or, absent that, __len__ — an object with a defined length is falsy if that length is zero). The literal values False, None, numeric zero (0, 0.0, 0j), and empty containers ("", [], (), {}, set()) are all falsy; virtually everything else is truthy.
Testing containers directly for truthiness (if items:) is the idiomatic way to check 'is this non-empty' — prefer it over if len(items) > 0: or if items != []:.
for value in [0, 1, '', 'text', [], [1], None, {}, {'a': 1}]:
if value:
print(f'{value!r} is truthy')
else:
print(f'{value!r} is falsy')Multiple Branches: elif Chains
Any number of elif clauses may follow an if, and Python evaluates them top to bottom, stopping at the first true condition — later elif/else clauses are never even evaluated once one branch has matched. Order matters: put the most specific or most likely conditions first when ranges overlap.
A common bug: writing conditions in the wrong order so an earlier, broader elif swallows cases meant for a later, narrower one. if score >= 70: ... elif score >= 90: would classify a 95 as a 'C' student, because score >= 70 matches first.
def grade_for(score: int) -> str:
if score >= 90:
return 'A'
elif score >= 80:
return 'B'
elif score >= 70:
return 'C'
else:
return 'F'
print(grade_for(85)) # 'B'Nested Conditions
if statements can nest inside other if/elif/else blocks to express conditions that depend on other conditions. Each level of nesting adds one indentation level. Deep nesting hurts readability, so it's often worth combining conditions with and/or or restructuring with early returns instead.
def can_checkout(cart_total: float, is_member: bool, in_stock: bool) -> str:
if in_stock:
if cart_total > 0:
if is_member or cart_total >= 50:
return 'proceed to checkout'
else:
return 'add $50 more or join membership for free shipping'
else:
return 'cart is empty'
else:
return 'item out of stock'# Flattened equivalent -- often clearer
def can_checkout_v2(cart_total: float, is_member: bool, in_stock: bool) -> str:
if not in_stock:
return 'item out of stock'
if cart_total <= 0:
return 'cart is empty'
if is_member or cart_total >= 50:
return 'proceed to checkout'
return 'add $50 more or join membership for free shipping'Combining Conditions with and / or / not
Complex conditions are built from boolean operators (see Operator Precedence & Evaluation for exact binding rules). Parenthesizing sub-conditions makes multi-part logic auditable at a glance, and short-circuiting means later operands are skipped once the result is already determined.
age = 25
has_license = True
has_insurance = False
if age >= 18 and has_license and (has_insurance or age >= 21):
print('can rent a car')
else:
print('cannot rent a car')Conditional (Ternary) Expressions as a Compact Alternative
When each branch is a single simple value (not a statement or side effect), a conditional expression value_if_true if condition else value_if_false can replace a short if/else — it's an expression, so it can be assigned, passed as an argument, or embedded in an f-string.
Reserve conditional expressions for simple value selection. Once logic involves multiple statements, side effects, or more than one condition, a full if/elif/else statement is clearer.
n = 7
parity = 'even' if n % 2 == 0 else 'odd'
print(f'{n} is {parity}')
# equivalent, longer statement form:
if n % 2 == 0:
parity = 'even'
else:
parity = 'odd'Practical Usage: Guard Clauses and Early Returns
A common, readable pattern is the guard clause: handle invalid or edge-case inputs first with an early return (or continue/raise), so the 'main' logic isn't buried under nested conditionals.
def safe_divide(a: float, b: float) -> float | None:
if b == 0:
return None # guard clause -- handle the edge case immediately
return a / b
print(safe_divide(10, 2)) # 5.0
print(safe_divide(10, 0)) # NoneEdge Cases
A few situations catch people off guard: comparing floats for exact equality (0.1 + 0.2 == 0.3 is False due to binary floating-point representation — use a tolerance or math.isclose instead), and testing is vs == for value comparisons (use == for value equality; reserve is for identity checks like x is None).
An if statement with an empty body is a SyntaxError in Python — there is no way to write 'do nothing' inline the way some languages allow an empty {}. Use the pass statement (covered in break/continue/pass) as an explicit no-op placeholder.
import math
print(0.1 + 0.2 == 0.3) # False -- floating-point representation
print(math.isclose(0.1 + 0.2, 0.3)) # True
value = None
if value is None: # correct: identity check for the None singleton
print('no value')Common Pattern: Dispatching Without a switch Statement
Python has no classic switch statement. Long elif chains work for simple dispatch, but once you're matching many discrete cases (especially with destructuring), match/case structural pattern matching (Python 3.10+, covered separately) or a dictionary mapping values to functions are often cleaner alternatives.
# elif chain for simple dispatch -- fine for a handful of cases
def describe(day: str) -> str:
if day == 'Sat' or day == 'Sun':
return 'weekend'
elif day in ('Mon', 'Tue', 'Wed', 'Thu', 'Fri'):
return 'weekday'
else:
return 'unknown'
# dictionary dispatch -- scales better for many exact-value cases
ACTIONS = {
'start': lambda: print('starting'),
'stop': lambda: print('stopping'),
}
ACTIONS.get('start', lambda: print('unknown action'))()Key Takeaways
if/elif/else branches are delimited by indentation, not braces; a consistent 4-space indent is standard style.
Conditions are evaluated for truthiness, not required to be literal booleans — falsy values are False, None, zero-valued numbers, and empty containers.
elif clauses are checked top to bottom, and only the first true branch runs — order conditions from most specific to least specific when ranges overlap.
Prefer guard clauses / early returns over deep nesting to keep branching logic flat and readable.
Conditional expressions (x if cond else y) are a compact alternative for simple value selection, not a replacement for full statement logic.
Python has no switch statement; match/case or dictionary dispatch are the idiomatic alternatives for many-way dispatch.
if x == True: ...
Comparing explicitly to True is redundant and can misbehave with non-bool truthy values (e.g. x = 2, where x == True is False but x itself is truthy). Just test the value directly.
if 0.1 + 0.2 == 0.3: print('equal') import math if math.isclose(0.1 + 0.2, 0.3): print('equal')
Binary floating-point cannot represent 0.1, 0.2, or 0.3 exactly, so direct equality often fails unexpectedly. Use math.isclose (or an explicit tolerance) for float comparisons.
if score >= 70: grade = 'C' elif score >= 90: grade = 'A' if score >= 90: grade = 'A' elif score >= 70: grade = 'C'
elif chains stop at the first true branch. Ordering broader conditions before narrower ones causes the narrower condition to never be reached.
if value == None: ...
if value is None: ...
None is a singleton; identity comparison with is is both the idiomatic and technically correct check, and avoids relying on a custom __eq__ that might behave oddly with None.
What determines whether the block under an if statement executes, if the condition isn't already a boolean?
Python evaluates the condition's truthiness via __bool__ (or __len__ as a fallback) — falsy values like 0, None, '', and empty containers make the block skip; everything else runs it.
Why is if x == True: considered poor style compared to if x:?
It's redundant, and for a value like x = 2 it actually gives the wrong answer: 2 == True is False even though 2 itself is truthy, so if x: would run the block but if x == True: would not.
Explain why elif order matters when ranges overlap, with an example.
elif chains stop at the first true condition. If broader conditions are placed before narrower ones (e.g. score >= 70 before score >= 90), the narrower branch is unreachable for values that also satisfy the broader one.
What is a guard clause, and why might it be preferred over deeply nested if statements?
A guard clause handles an edge case with an early return/continue/raise at the top of a function, so the main logic path isn't buried under multiple levels of indentation — it keeps the common case flat and readable.
Why does if 0.1 + 0.2 == 0.3: evaluate to False, and what's the idiomatic fix?
Binary floating-point representation cannot exactly represent most decimal fractions, so 0.1 + 0.2 yields a value slightly different from the literal 0.3. Use math.isclose() or an explicit epsilon tolerance instead of exact equality.
When is a conditional expression (a if cond else b) preferable to a full if/else statement, and when is it not?
It's preferable for simple value selection assigned to a variable, passed as an argument, or embedded in an f-string. It's not appropriate once branches need multiple statements, side effects, or more than one condition — a full statement is clearer there.
What does this print and why?
'empty or falsy'. An empty list is falsy in Python (its __len__ is 0), so the else branch runs.
Write a function classify_triangle(a, b, c) that returns 'equilateral', 'isosceles', or 'scalene' based on the three side lengths, using if/elif/else.
def classify_triangle(a: float, b: float, c: float) -> str: if a == b == c: return 'equilateral' elif a == b or b == c or a == c: return 'isosceles' else: return 'scalene' print(classify_triangle(3, 3, 3)) # 'equilateral' print(classify_triangle(3, 3, 5)) # 'isosceles' print(classify_triangle(3, 4, 5)) # 'scalene'
Chained equality a == b == c checks all three sides equal in one expression; or then covers any two-sides-equal case; anything else falls to scalene.
What happens when this code runs, and why?
It raises UnboundLocalError: local variable 'result' referenced before assignment. When n == 0, neither branch executes, so result is never assigned before the return result line runs. This is a common bug from omitting an else (or a default assignment) to cover every case.
Rewrite this deeply nested function using guard clauses (early returns) to reduce nesting: it returns 'approved' only if a user is active, verified, and has a balance over 0.
def original(is_active, is_verified, balance): if is_active: if is_verified: if balance > 0: return 'approved' else: return 'insufficient balance' else: return 'not verified' else: return 'inactive account' def with_guards(is_active: bool, is_verified: bool, balance: float) -> str: if not is_active: return 'inactive account' if not is_verified: return 'not verified' if balance <= 0: return 'insufficient balance' return 'approved'
Guard clauses handle each failure condition immediately with an early return, flattening three nested levels into a single sequence of independent checks — functionally identical but easier to scan and modify.
x = []
if x:
print('non-empty')
else:
print('empty or falsy')def check(n):
if n > 0:
result = 'positive'
elif n < 0:
result = 'negative'
return result
print(check(0))