How does Python parse `'Result: ' + status if ok else 'fail'`, and why is that a bug?
answer
- Almost nothing binds looser than it
- Only lambda sits below it
- The plus is swallowed into a branch
- Comma is looser still — accidental tuple
- Parenthesize whenever it is an operand
basics
~20 sThe conditional expression binds looser than +, so Python reads it as ('Result: ' + status) if ok else 'fail'. When ok is falsy the prefix silently disappears. Parenthesize the conditional expression so it is a single operand of the concatenation.
solid answer
~40 sA conditional expression sits almost at the bottom of Python's precedence table — only `lambda` binds looser. So every arithmetic operator, comparison, `not`, `and` and `or` around it binds **tighter**, and the parse becomes `('Result: ' + status) if ok else 'fail'`: the whole concatenation is the true-branch, and the false-branch is a bare `'fail'` with no prefix. The fix is parentheses: `'Result: ' + ('pass' if ok else 'fail')`. The same trap shows up with the comma, which binds even looser — `x = 1 if flag else 2, 3` builds the tuple `(2, 3)` when `flag` is falsy, because the comma is outside the conditional expression. My rule is to parenthesize any conditional expression that is not the entire right-hand side of an assignment, a `return`, or a single call argument.
code
pycon · 5 lines>>> ok = False
>>> "Result: " + "pass" if ok else "fail"
'fail'
>>> "Result: " + ("pass" if ok else "fail")
'Result: fail'go deeper
Remember that a conditional expression grabs as much as it can on both sides. When you mix one with + or a comma, add parentheses around it and read the line again before committing.
Explain the grammar: the parts around if parse at or_test level and the else operand is a full expression, so only lambda and the comma are looser. Show the concatenation and accidental-tuple examples and the parenthesized fix.
Treat this as a silent wrong-value class, not a crash: the mis-grouped branch is usually the rare one, so it escapes review and reaches production. Argue for a parenthesization convention and for tests that cover both branches, since no linter flags legal code.
Decide the house rule and make it cheap to follow — bare conditional expressions only as a whole right-hand side, return value or argument — and weigh whether such expressions belong in hot, frequently edited code at all when an if statement would make the grouping unmistakable.
### Where the form sits in the precedence table Python's grammar defines a conditional expression roughly as `or_test if or_test else expression`. Two facts follow from that single line, and every surprise in this area is one of them. **Fact one: everything to the left of `if`, and the condition itself, is parsed at `or_test` level.** That means arithmetic, string concatenation, comparisons, `not`, `and` and `or` all bind tighter than the conditional expression and get swallowed into the operand. Only `lambda` — and the comma, which is not an operator at all but a separator in the surrounding syntax — is looser. **Fact two: the `else` operand is a full expression**, which is what makes chains right-associative and also means a trailing `+ 3` lands inside the else branch: `1 if flag else 2 + 3` is `1 if flag else (2 + 3)`, never `(1 if flag else 2) + 3`. ### The concatenation trap ```pycon >>> ok = False >>> "Result: " + "pass" if ok else "fail" 'fail' ``` The author meant the prefix to be constant and only the suffix to vary. Python read it as `("Result: " + "pass") if ok else "fail"`, so the falsy path returns a string with no prefix. Nothing raises; the output is merely wrong, and it is wrong only on the path that is exercised least. The same shape bites with `%`-style building, with `+` on numbers (`base + 10 if promoted else 0` gives `0`, not `base`), and inside a longer boolean chain (`a or b if c else d` is `(a or b) if c else d`, which is usually not what someone typing it meant). ### The comma trap The comma builds tuples and separates arguments, and it is looser than a conditional expression: ```pycon >>> x = 1 if False else 2, 3 >>> x (2, 3) ``` The conditional expression produced `2`, and the comma then made a two-tuple with `3`. In a call the comma is a separator, so `send(1 if flag else 2, 3)` passes two arguments and is fine — but `return 1 if flag else 2, 3` returns a tuple, which is a classic accidental-tuple defect in a function whose caller expects a single value. ### Where parentheses are outright required A few positions do not merely benefit from parentheses, they demand them: - **Inside a `lambda` body you must parenthesize to stop the lambda swallowing the whole thing**: `lambda n: n if n > 0 else 0` is fine because the body is the entire conditional expression, but `sorted(items, key=lambda n: (n if n > 0 else 0, n))` needs the parentheses to keep the tuple honest. - **As a `yield` value or in any position that expects a restricted expression**, wrap it. - **As a comprehension's filter clause**: the filter after `for` is parsed at `or_test` level, so `[x for x in xs if (a if b else c)]` needs the parentheses. - **When you want a conditional expression to be one operand of a binary operator**, which is the concatenation case above. ### f-strings A conditional expression is a normal expression inside an f-string replacement field: `f"{n} item{'' if n == 1 else 's'}"` works. Before Python 3.12 the inner string literal had to use a different quote character from the enclosing f-string, and the field could not span lines; PEP 701 in 3.12 formalized f-string parsing so the same quote character and multi-line fields are now allowed. Code that must run on older interpreters still needs the alternating quotes. ### How to read a suspicious line When a line mixes a conditional expression with anything else, mentally insert the parentheses the grammar implies: put brackets around everything from the start of the expression up to the `if`, and around everything after the `else` to the end of the enclosing expression. If those brackets are not where you wanted them, type them for real. Formatters and linters will not fix this for you — the code is legal, so no tool objects; the only defence is the habit of parenthesizing. ### The reviewer's rule A practical convention that removes the whole class of bug: a conditional expression may appear bare only when it is the *entire* right-hand side of an assignment, the *entire* value of a `return`, or an *entire* single call argument. In every other position — an operand, a tuple element, a comprehension filter, an f-string field mixed with other text — it is parenthesized. The parentheses cost nothing at runtime, they are ignored by the compiler beyond grouping, and they turn a silent wrong-value bug into an obviously-correct line.
- How does Python parse `1 if flag else 2 + 3`?As `1 if flag else (2 + 3)`. The `else` operand is a full expression, so the addition is absorbed into the false branch and the result is `1` or `5` — never `4`. If you wanted to add to the whole choice, write `(1 if flag else 2) + 3`.
- Why does `return 1 if flag else 2, 3` return a tuple?The comma is looser than the conditional expression, so the `return` sees an expression list: the conditional expression is the first element and `3` is the second. The function returns `(1, 3)` or `(2, 3)`. Parenthesize the conditional expression, or drop the stray comma.
- Will a linter or formatter catch this class of mistake?No. The code is syntactically legal and type-consistent, so nothing objects; a formatter may even reflow the line without adding grouping parentheses. The defence is a convention — parenthesize any conditional expression that is not the entire right-hand side, the entire return value, or a whole argument — plus tests that exercise both branches.
saying these in an interview costs you the question
- Thinks the conditional binds tighter than arithmetic
- Reads the line strictly left to right as written
- Says the parentheses are only cosmetic here
- Misses that a trailing comma builds a tuple
- Expects a linter or formatter to catch the mis-grouping
- Believes the else branch inherits the prefix automatically