In Python's `a if cond else b`, what is evaluated and in what order?
answer
- It produces a value, not a step
- The test runs before either branch
- One side never executes at all
- No one-armed form exists
- Unlike a call, not both operands
basics
~20 sPython evaluates cond first. If it is truthy, only a is evaluated and becomes the value; otherwise only b is. The branch that is not chosen never runs, so its side effects and its exceptions never happen.
solid answer
~50 sThe form is `a if cond else b`, and it is an **expression**: it produces a value rather than performing a step. Evaluation order is condition first, chosen branch second — the middle operand `cond` is tested for truth, then exactly one of `a` or `b` is evaluated and returned. The other operand is never evaluated at all, which is why `10 / x if x else None` is safe: the division is skipped when `x` is zero, instead of raising `ZeroDivisionError`. This laziness is what separates it from an ordinary function call, where every argument is evaluated before the call happens. The `else` part is mandatory — there is no one-armed conditional expression — and because it yields a value, it can sit where a statement cannot: inside a call argument, a comprehension, an f-string replacement field, a `return`, or a parameter's default expression.
code
python · 8 linesdef loud(label, value):
print("evaluated", label)
return value
flag = True
result = loud("left", 1) if flag else loud("right", 2)
print("result", result)go deeper
Recall the shape and the order: condition first, then exactly one branch, and the else is required. Be able to rewrite a short if/else assignment as one line and back again.
Explain that the untaken branch never executes, and use that to guard a division or an attribute access. Know that a call evaluates all its arguments first, so wrapping the choice in a helper destroys the guard.
Show judgement about when the one-liner helps and when an if statement reads better, and point out the latent-bug risk: both branches compile, so a name error in the rarely taken side surfaces only in production unless tests cover both paths.
Own the convention for the codebase — where the form is welcome (defaults, arguments, comprehension elements) and where it is banned (anything a reviewer must re-read), and make "both branches covered by tests" part of the review expectation rather than a matter of taste.
### The form and its grammar Python's conditional expression is written `a if cond else b`, sometimes called the ternary operator because it takes three operands. Unlike an `if` statement, it is an **expression**: it evaluates to a value, so it can appear anywhere a value is allowed. The word order is deliberate — the common-case result comes first, the test second — which reads like English ("return the cached value if it is present, otherwise recompute") and is why Python did not adopt C's `cond ? a : b` punctuation. ### Evaluation order The order is fixed and worth stating precisely, because it is not the left-to-right order of the written text: 1. `cond` is evaluated first, even though it is written in the middle. 2. Its result is tested for truth. 3. If the test passes, `a` is evaluated and its value becomes the value of the whole expression. 4. Otherwise `b` is evaluated and its value becomes the value of the whole expression. Step 3 and step 4 are exclusive. Exactly one of the two branches runs — never both, never neither. ### Laziness is the point The untaken branch is not merely discarded after being computed; it is never computed. Nothing in it executes: no function is called, no attribute is looked up, no exception is raised, no counter is incremented. That makes the conditional expression a **guard**, not just a chooser: ```python rate = total / count if count else 0.0 ``` When `count` is zero, the division does not happen, so there is no `ZeroDivisionError` to catch. Contrast this with a function call: ```python def pick(first, second, use_first): return first if use_first else second value = pick(total / count, 0.0, count > 0) # still raises when count == 0 ``` Here both arguments are evaluated before `pick` is entered, because argument evaluation in a call is eager. Wrapping a choice in a helper function throws the laziness away — a common and subtle regression when someone "tidies" a conditional expression into a utility. If you need deferred evaluation across a function boundary you must pass callables and invoke the chosen one, which is usually more machinery than simply keeping the conditional expression at the call site. ### Both branches must still be valid code Laziness is a runtime property, not a compile-time one. Both operands are compiled, so both must parse and both must reference names that exist by the time they might be evaluated. `x if flag else undefined_name` compiles fine and only raises `NameError` when `flag` is false — a latent bug that a test exercising one branch will not catch. Cover both branches in tests. ### The `else` is mandatory There is no one-armed form. `value = a if cond` is a `SyntaxError`. If you genuinely want "use `a` when the condition holds, otherwise leave the name alone", that is an `if` statement, not an expression — the expression must produce a value in every case, so it must say what that value is when the test fails. ### Where it fits that a statement cannot Because it is an expression, it goes in positions that forbid statements: - a call argument: `send(payload, timeout=fast if urgent else slow)` - an element expression inside a comprehension - an f-string replacement field: `f"{count} item{'' if count == 1 else 's'}"` - a `return` line, a `lambda` body, a `yield` value - a parameter's default expression in a `def` header, evaluated once at definition time like any other default That last one is worth care: putting a conditional expression in a default expression does not make the default lazy. `def f(limit=HIGH if DEBUG else LOW)` evaluates the whole conditional expression once, when the `def` executes, and stores the single resulting value on the function. If you want the choice to be made per call, make the default a sentinel such as `None` and resolve it in the body, where the conditional expression is re-evaluated on every call. ### Truth testing, briefly The condition is tested for truth the same way an `if` statement tests it — an empty container, zero, `None` and `False` all fail the test. Note the consequence for the result value: `count if count else default` returns `default` for a legitimate zero, while `count if count is not None else default` distinguishes "zero" from "missing". Choosing the right test is usually more important than choosing the right branch values. ### Cost There is no hidden overhead. CPython compiles the form into a truth test and a jump, exactly as it compiles the corresponding `if` statement; picking one over the other is a readability decision, not a performance one.
- Why does moving `a if cond else b` into a helper function change its behaviour?Because argument evaluation in a call is eager. `pick(risky(), fallback, cond)` evaluates `risky()` before `pick` is entered, so the guard is gone and the exception you were avoiding is raised again. The conditional expression only defers work while it stays at the point of use; to defer across a call you must pass callables and invoke the chosen one.
- Is there a one-armed version, `x = a if cond`?No — that is a `SyntaxError`. An expression must produce a value on every path, so the `else` branch is required. "Do something only when the condition holds" is a job for an `if` statement, which performs an action rather than producing a value.
- Does putting a conditional expression in a parameter's default make the default lazy?No. A default expression in a `def` header is evaluated once, when the `def` statement runs, and the single resulting value is stored on the function. The conditional expression picks a branch at definition time, not per call. For a per-call decision, default to a sentinel such as `None` and resolve it inside the body.
It is a fork in a road, not a menu: you read the signpost first, then walk exactly one way, and the road you did not take is never travelled.
saying these in an interview costs you the question
- Says both branches are evaluated and one result discarded
- Thinks the branches are read left to right before the test
- Believes `x = a if cond` is legal without an else
- Claims the form is faster than an equivalent if statement
- Assumes an undefined name in the unused branch is harmless forever
- Thinks wrapping it in a helper function preserves the laziness