PYTHON / CONTROL FLOW
Conditional expressions
Use Python's `A if cond else B` expression to produce a value inline, and read its precedence and evaluation order correctly.
What you will learn
- Write `value_if_true if condition else value_if_false` and know the condition runs first
- Place a choice directly in arguments, f-strings, return values and comprehensions
- Explain why `else` is mandatory and why only one branch is ever evaluated
- Add parentheses when arithmetic sits next to a conditional expression
Understanding Conditional expressions
A conditional expression has the shape `A if C else B`, and unlike an `if` statement it evaluates to a value rather than directing which block runs. Python evaluates `C` first even though it is written in the middle, then evaluates exactly one of `A` or `B` and hands that object back. This is why `else` is not optional: an expression must produce something, and there is no invisible fallback value to supply when the condition is false. Leaving it off is a SyntaxError, not a silent `None`.
Because the whole thing is an expression, it fits anywhere a value fits: an argument (`open(path, 'w' if overwrite else 'a')`), an f-string replacement field, a dict value, a `return`, or the element part of a comprehension. That last one is the common case where an `if` statement simply cannot go, since the trailing `if` of a comprehension filters items out instead of choosing between two results. The unevaluated branch is never touched, so `x[0] if x else 'empty'` is safe even though `x[0]` would raise on an empty list.
The trap is precedence. A conditional expression binds looser than arithmetic, comparisons, `and` and `or`, so `base + 10 if premium else 0` groups as `(base + 10) if premium else 0` and the false case throws `base` away entirely. It binds tighter than `lambda` and assignment, so `f = lambda n: 'even' if n % 2 == 0 else 'odd'` needs no parentheses. Nesting associates to the right, making `a if p else b if q else c` a readable cascade, but past one nesting level an `if`/`elif` block is clearer.
def plural(n):
return f'{n} item' if n == 1 else f'{n} items'
for n in (0, 1, 5):
print(plural(n))
counts = [4, 0, 10, 0]
rates = [100 // c if c else None for c in counts]
print(rates)
def boom():
raise ValueError('not evaluated')
print('yes' if True else boom())
A conditional expression chooses between two values rather than two blocks, which is why it can appear inside other expressions and why its `else` is required.
Worked examples
Precedence and nesting
Shows how arithmetic next to a conditional expression groups, and how nested conditionals read left to right.
x = 2
print(1 + x if x > 5 else 0)
print(1 + (x if x > 5 else 0))
grade = 'high' if x > 100 else 'medium' if x > 5 else 'low'
print(grade)
Example explained
Line 1`1 + x if x > 5 else 0` parses as `(1 + x) if x > 5 else 0`, so a false condition yields plain `0` and the `1 +` disappears.
Line 2Parenthesising the conditional makes it an operand of `+`, so the result is `1 + 0` and the addition always happens.
Line 3The nested form groups as `'high' if x > 100 else ('medium' if x > 5 else 'low')`; both tests fail for 2, so the innermost `else` wins.
Defaults: `or` versus an explicit test
Demonstrates why `or` is not a safe substitute for a conditional expression when zero or empty values are legitimate.
def width_or(w):
return w or 80
def width_if(w):
return w if w is not None else 80
for w in (120, 0, None):
print(w, '->', width_or(w), width_if(w))
Example explained
Line 1`w or 80` returns 80 for any falsy `w`, so a deliberate width of 0 is silently replaced.
Line 2`w if w is not None else 80` tests identity against `None`, so 0 survives as a real value.
Line 3Both agree for 120 and `None`, which is exactly why the `or` bug hides in normal test data.
Important notes
A branch must be a single expression: you cannot put `raise`, `pass` or an assignment statement in it, though a walrus (`:=`) is allowed since it is an expression.
`do_a() if c else do_b()` is legal but throws away both return values; when you want an action rather than a value, an `if` statement communicates that better.
Common mistakes
Omitting `else` (`status = 'ok' if healthy`), which raises SyntaxError because an expression has no value to return when the condition is false.
Writing the condition first, as in `label = flag if 'yes' else 'no'`, which parses fine and returns `flag` or `'no'` because `'yes'` is truthy, so the bug shows up as wrong data rather than an error.
Forgetting precedence in `total = subtotal + fee if express else 0`, which sets `total` to 0 for non-express orders and loses the subtotal.
Try it yourself
Change, predict, then run
Write `sign(n)` that returns `'positive'`, `'negative'` or `'zero'` using one nested conditional expression, then print `sign(-3)`, `sign(0)` and `sign(7)`. Next, build `[n if n > 0 else 0 for n in [-2, 5, -7, 3]]` and confirm the negatives become 0 instead of being filtered out.
Open the Python workspaceCheck your understanding
Why does a conditional expression require `else` when an `if` statement does not?
- An expression must evaluate to some object no matter which way the condition goes, and there is no default value to fall back on.
- Without `else` the parser cannot tell where the expression ends.
- Python would have to return `None`, which is slower than returning a real value.
- The `else` branch is evaluated first, so it must always be present.
Show answer
The construct is an expression, so it has to hand a value back to whatever surrounds it; with no `else` there would be nothing to hand back when the condition is false. The `None` option is tempting because a function whose `if` has no `else` does return `None`, but that is a statement-level rule about falling off the end of a function, not a performance issue, and an expression has no such fall-through path. The last option is wrong about order: the condition is evaluated first, then exactly one branch.