Conditionals
Python’s conditional constructs are if/elif/else and the conditional expression ... if ... else .... Both are pervasive; the conditional expression admits inline branching where a value is needed, and the statement form admits multi-way branching with arbitrary statement bodies. Python’s truthiness rules — every value implicitly has a boolean interpretation — affect every condition; the conventional Python idiom often relies on the truthiness of containers, numbers, and None to write compact tests. The match statement (covered in Pattern matching) admits richer discrimination than if/elif chains for some cases.
if, elif, else
The principal form:
if condition:
body1
elif condition2:
body2
else:
body3
The blocks are delimited by indentation; each branch executes if its condition is True. The elif (short for “else if”) admits cascading conditions without nested else: if.
def classify(n: int) -> str:
if n > 0:
return "positive"
elif n < 0:
return "negative"
else:
return "zero"
Python does not have a switch statement; chained elif is the conventional substitute for multi-way discrimination on values. For richer pattern-based dispatch, the match/case form is treated in Pattern matching.
Truthiness
Python’s if-condition need not be a bool. Every value has a truth value:
| Falsy values | Truthy values |
|---|---|
False, None | True |
0, 0.0, 0j | non-zero numbers |
"", [], (), {}, set() | non-empty containers |
Custom objects with __bool__ returning False | everything else |
The conventional idioms:
items = [1, 2, 3]
if items: # equivalent to: if len(items) > 0
print("non-empty")
name = ""
if not name: # equivalent to: if name == ""
print("empty name")
count = None
if count is None: # explicit None test
count = 0
The None-test is conventionally written is None (identity), not == None. The is form is faster and explicit.
For container emptiness, the truthiness shortcut (if items:) is conventional. The explicit if len(items) > 0: is admitted but verbose.
pass for empty blocks
Python’s grammar requires every block to have at least one statement. The pass statement is the no-op:
class Stub:
pass
def to_be_implemented():
pass
if condition:
do_something()
else:
pass # explicitly do nothing
pass is the conventional placeholder for empty bodies during development.
The conditional expression
Python’s ternary form is value_if_true if condition else value_if_false:
result = "positive" if n > 0 else "non-positive"
The order is unconventional: the value-when-true comes first, then the condition, then the value-when-false. The grammar reads naturally: “this if cond else that”.
The form is an expression, so it admits use anywhere a value is expected:
display_name = name if name else "anonymous"
size_label = "large" if size > 100 else "small"
print("ok" if status else "error")
Nested conditional expressions are admitted but quickly become unreadable; one or two levels is typically the practical limit.
sign = "+" if x > 0 else "-" if x < 0 else "0"
The above is right-associative — equivalent to "+" if x > 0 else ("-" if x < 0 else "0"). For more elaborate logic, an if/elif/else block is conventionally clearer.
Boolean operators in conditions
The boolean operators and, or, not are word-spelt. They short-circuit:
if x is not None and x > 0: # x > 0 is checked only if x is not None
process(x)
if user.is_admin or user.is_owner:
grant_access()
if not config.debug:
log.disable_verbose()
The operators return one of their operands, not necessarily a bool:
config.get("name") or "default" # returns the name or the default
items and items[0] # returns the first item or the empty container
The conventional Python pattern value or default admits “use the value if truthy, otherwise the default”.
For None-aware fallbacks where falsy values (e.g., 0, "") should not trigger the fallback, the explicit form is needed:
result = a if a is not None else default # clear None test
result = default if a is None else a # equivalent
Chained comparisons
Python admits chained comparisons:
if 0 < x < 10:
print("in range")
if a < b < c:
print("ordered")
if username == old_name == new_name:
print("unchanged")
Each operand is evaluated at most once; the chain short-circuits on the first false. The mechanism reads more naturally than 0 < x and x < 10.
Walrus operator
The := (walrus) admits assignment within a condition:
if (n := compute()) > 100:
print(f"large: {n}")
process(n)
while (line := input("> ")) != "quit":
handle(line)
The walrus binds the name and uses the value in the condition. The conventional uses are:
- Capturing a value used in both the condition and the body.
- Read-loops where the read result terminates the loop.
- Comprehensions where the same computation is used multiple times.
The walrus is rare in routine code; the explicit n = compute() followed by if n > 100: is conventionally clearer.
match for pattern matching
Python 3.10 introduced structural pattern matching with match/case:
def describe(point):
match point:
case (0, 0):
return "origin"
case (x, 0):
return f"x axis at {x}"
case (0, y):
return f"y axis at {y}"
case (x, y):
return f"({x}, {y})"
case _:
return "not a point"
The full treatment is in Pattern matching. The principal point: match admits richer discrimination than if/elif for cases where the structure of the value (not just its boolean nature) matters.
Selection idioms
Several patterns recur in idiomatic Python.
Early return for guard clauses
def process(input: str) -> Result:
if not input:
return Result.empty()
if len(input) > MAX_LENGTH:
return Result.too_long()
if not is_valid(input):
return Result.malformed()
# main body
return compute(input)
The pattern reduces nesting and keeps the precondition checks visually separate from the substantive body. The conventional Python form for “validate, then process”.
Ternary for inline selection
sep = ", " if len(items) > 1 else ""
name = first or "anonymous"
divisor = denom if denom != 0 else 1
The conditional expression is the conventional choice for short selections within larger expressions.
Truthiness for empty/non-empty checks
if items:
process(items)
if not items:
show_empty_state()
The conventional Python idiom for testing container emptiness.
is None for None checks
if value is None:
value = default
if not (value is None):
process(value) # equivalent to: if value is not None:
The is None and is not None forms are conventional; == None is admitted but rarely seen in idiomatic code.
match for value-and-shape dispatch
def handle_event(event):
match event:
case {"type": "click", "x": x, "y": y}:
handle_click(x, y)
case {"type": "key", "code": code}:
handle_key(code)
case _:
log_unknown(event)
The match form admits dispatching on both type and structure; the full treatment is in Pattern matching.
dict-based dispatch (for older Python)
For pure value-to-value dispatch:
DAY_NAME = {
0: "Monday",
1: "Tuesday",
2: "Wednesday",
# ...
}
name = DAY_NAME.get(day, "unknown")
The dict-lookup pattern is the pre-3.10 substitute for switch-style dispatch on a small set of values.
For value-to-function dispatch:
HANDLERS = {
"click": handle_click,
"key": handle_key,
"scroll": handle_scroll,
}
handler = HANDLERS.get(event_type, handle_unknown)
handler(event)
The pattern admits adding new handlers without modifying the dispatch logic.
A note on the absence of switch
Python did not have a switch statement until 3.10’s match/case. The conventional pre-match idioms:
if/elifchain — the most common.dictof value-to-value or value-to-function.- Polymorphism — a class hierarchy where each subclass implements the operation.
Modern Python (3.10+) admits match for richer discrimination. For pure value comparisons, if/elif remains the conventional choice; for structural matching (extracting parts of values), match is the contemporary form.