Does Python have a ternary conditional operator?
Yes, and the syntax reads differently from every other language that has one. This article shows how it works, and why the two older idioms you still find in the wild are not equivalent to it.
The syntax
value = "adult" if age >= 18 else "minor"
'adult' if age >= 18 else 'minor' -> adult
with age = 12 -> minor
The condition sits in the middle, not at the front. This trips up anyone
arriving from C, Java, PHP or JavaScript, where it is cond ? a : b.
The reasoning behind the order, from PEP 308, is that the common value comes first and reads like English: value, if condition, otherwise other value.
Only one branch is evaluated
Worth demonstrating, because it is what makes the construct safe:
evaluating TRUE-branch
result = 1
-> the false branch was never evaluated
So this is fine even when the divisor is zero:
result = "safe" if b == 0 else a / b
Which is why the tuple trick is not the same thing
Before Python 2.5 people wrote this:
value = (false_value, true_value)[condition]
It works because False and True are 0 and 1. But:
(false_value, true_value)[condition]:
evaluating index 0
evaluating index 1
-> BOTH were evaluated; the tuple has to be built first
Both branches ran. Of course they did — you cannot index a tuple that has not been built yet.
With side effects that is wasteful. With anything that can fail it is a bug:
tuple trick -> ZeroDivisionError: division by zero
ternary -> safe
The two constructs are not interchangeable. One of them raises.
The and/or idiom, and where it breaks
The other pre-2.5 workaround:
value = condition and true_value or false_value
True and 'yes' or 'no' -> yes
False and 'yes' or 'no' -> no
Looks correct. Now make the true-value falsy:
True and '' or 'fallback' -> 'fallback'
'' if True else 'fallback' -> ''
The condition was True, and it still returned the fallback. Because
True and '' is '', which is falsy, so or moves on to the right-hand side.
It fails for every falsy value:
True and 0 or 'FALLBACK' -> 'FALLBACK'
True and '' or 'FALLBACK' -> 'FALLBACK'
True and [] or 'FALLBACK' -> 'FALLBACK'
True and None or 'FALLBACK' -> 'FALLBACK'
True and False or 'FALLBACK' -> 'FALLBACK'
Empty strings, zero, empty lists and None are exactly the values that turn up
in real data. If you meet this idiom in old code, treat it as a bug waiting for
the right input.
The precedence will catch you out
The conditional binds very loosely — more loosely than arithmetic:
1 + 2 if False else 99 -> 99
Not 1 + 99. Python read that as (1 + 2) if False else 99, so the whole
1 + 2 is the true-branch and the result is just 99.
What you probably meant:
1 + (2 if False else 99) -> 100
The rule of thumb is to parenthesise the conditional whenever it is part of a larger expression. It costs two characters and removes the question.
It is an expression, so it goes where an if statement cannot
This is the real reason to use it rather than a four-line if:
in a comprehension : [1, 0, 3, 0]
as an argument : 3
in an f-string : 4 items
as a default : default
The f-string case is the one I reach for most:
f"{n} item{'s' if n != 1 else ''}"
Ternary or filter in a comprehension?
A frequent follow-up, and the answer is positional:
[x if x > 0 else 0 for x in values] : [1, 0, 3, 0]
[x for x in values if x > 0] : [1, 3]
Before the for it is a conditional expression, and the else is
mandatory — you are choosing what each element becomes. After the for it
is a filter clause, and an else is a syntax error, because there is nothing
to put there:
[x for x in values if x > 0 else 0] -> SyntaxError
Different jobs: the first maps four items to four items, the second selects two out of four.
Chaining works but stops being readable
def grade(score):
return "A" if score >= 90 else "B" if score >= 80 else "C" if score >= 70 else "F"
grade(95) -> A
grade(85) -> B
grade(75) -> C
grade(65) -> F
It nests to the right, so it does what it looks like. Past two branches I would
write an if/elif chain, or use
match
if the branches are patterns rather than ranges.
What about a null-coalescing operator?
Python has no ??. The nearest thing is or, and it is not the same:
value=None 'default' if value is None -> 'default' value or 'default' -> 'default'
value=0 'default' if value is None -> 0 value or 'default' -> 'default'
value='' 'default' if value is None -> '' value or 'default' -> 'default'
value='set' 'default' if value is None -> 'set' value or 'default' -> 'set'
or replaces every falsy value, not just None. If 0 or an empty string are
legitimate values in your data — a quantity, a discount, a submitted form field
— then x or default silently throws them away, and
x if x is not None else default is what you want.
About Netcup (advertisement)
The German host Netcup offers, among other things, affordable and powerful web hosting packages, KVM-based root servers and dedicated servers. With our voucher codes you can save even more (6€ off your first order, 30% off all KVM-based root servers, ...).