Python’s one-line if else is a conditional expression, written value_if_true if condition else value_if_false, and it produces a value rather than controlling a block.
That word — expression — is the whole thing. It is not a compressed if statement. It evaluates to something, which is why it can sit on the right of an assignment, inside a function call, or in a comprehension, and why it cannot contain a return, an assignment, or a raise.
Table of contents
- The syntax, and the odd word order
- Where it genuinely reads better
- Chaining, and why it goes wrong
- What it cannot do
- The or-shortcut, and the bug it hides
- A rule that survives code review
- How this fits the rest of the stack
- FAQ
The syntax, and the odd word order
status = "active" if user.is_enabled else "suspended"
The condition sits in the middle, which trips up anyone arriving from C-style cond ? a : b. Python puts the common case first on the theory that you read the likely result, then the qualification. Whether that is more readable is a matter of taste; it is what the language does.
Evaluation is lazy and only one branch runs. This matters more than it sounds:
# safe: the division is never evaluated when count is 0
avg = total / count if count else 0
If both branches were evaluated the way function arguments are, that line would raise ZeroDivisionError every time count was zero. It does not, because the conditional expression picks a branch and evaluates only that one.
Where it genuinely reads better
The strongest case is a small assignment that would otherwise take four lines to say one thing.
# four lines that hold one idea
if retries > 3:
delay = 60
else:
delay = 5
# the same idea
delay = 60 if retries > 3 else 5
Also strong inside a call, where a block would force you to invent a temporary variable:
logger.info("processed %d %s", n, "item" if n == 1 else "items")
And inside comprehensions, where it is the only way to transform conditionally. Note the position — this is the one place where the ordering genuinely matters:
# ternary BEFORE the for: transform every element
labels = ["even" if n % 2 == 0 else "odd" for n in numbers]
# plain if AFTER the for: filter elements out
evens = [n for n in numbers if n % 2 == 0]
Those two do completely different things and people mix them up constantly. Before the for, you are choosing a value and the output length is unchanged. After the for, you are choosing whether an element survives, and the output gets shorter.
Chaining, and why it goes wrong
There is no elif inside a conditional expression. You chain by putting another conditional expression in the else branch.
grade = "A" if score >= 90 else "B" if score >= 80 else "C" if score >= 70 else "F"
That parses as right-associative nesting — "A" if score >= 90 else ("B" if score >= 80 else (...)) — so it works correctly. It is also the point where the technique stops paying for itself. Three conditions in one line means the reader is holding three thresholds in their head while scanning left to right for the boundaries.
Two branches: fine. Three: borderline, and only if each piece is short. Four or more: write the block, or reach for a lookup structure.
# clearer than a four-deep chain
for threshold, letter in ((90, "A"), (80, "B"), (70, "C")):
if score >= threshold:
grade = letter
break
else:
grade = "F"
What it cannot do
The expression-versus-statement line is a hard boundary, and the errors are not always obvious.
# SyntaxError -- return is a statement
return 1 if x else raise ValueError("nope")
# SyntaxError -- assignment is a statement
(a = 1) if x else (a = 2)
You can return the result of a conditional expression, which is the shape people actually want:
def fee(plan):
return 0 if plan == "free" else 19
For the raise case, use or short-circuiting only when the falsy value is genuinely impossible, and otherwise write the if. Cleverness here buys nothing and costs a reader.
The or-shortcut, and the bug it hides
You will see this pattern used as a shorter default:
name = supplied_name or "anonymous"
It is not the same as a ternary. or tests truthiness, so it replaces every falsy value — empty string, 0, empty list, False, and None all get swapped for the default.
# 0 is a legitimate value, and this silently destroys it
timeout = supplied_timeout or 30 # supplied_timeout=0 -> 30
# says what it means
timeout = 30 if supplied_timeout is None else supplied_timeout
This is a real bug class, not a style nit. Anywhere 0, "", or an empty collection is a meaningful value, the or shortcut will eat it, and the resulting behaviour looks like a configuration problem rather than a code problem.
A rule that survives code review
One condition, both branches short, result assigned or passed — use the one-liner. Anything with a side effect, more than two branches, or a line that wraps — use the block.
The measure is not character count. It is whether a reader who has never seen the code can get the logic in one pass. A 90-character ternary that reads cleanly beats a nested block. A 50-character ternary with three chained conditions does not.
This kind of small consistency is the difference between config code you can skim and config code you have to decode — and skimmability is what you actually want at 2am when something is failing and you are reading a request handler for the first time in six months.
How this fits the rest of the stack
Readable request handlers are cheaper to operate, but they are not the only thing that decides what an app costs to run. The runtime, the database, the storage, and the bandwidth each carry a number, and the total is what shows up monthly. The RunxBuild hosting calculator puts those line items side by side so you can model them before committing.
Useful related references:
- Uninstall Python Cleanly: The Version You Can Remove, and the One You Must Not
- Python set add: One Element with add, Many with update
- Python Get UUID: uuid4, uuid7, and Which One for a Primary Key
- Python services on RunxBuild
FAQ
What is the syntax for a one-line if else in Python?
value_if_true if condition else value_if_false. The condition sits in the middle, unlike C-style ternaries. It is an expression, so it evaluates to a value and can be assigned, passed as an argument, or used inside a comprehension.
Can you use elif in a Python ternary?
No. Chain by nesting another conditional expression in the else branch: a if c1 else b if c2 else c. It parses right-associatively and works correctly, but readability drops fast past two conditions. Three or more is usually a signal to write the block.
Why can I not use return or raise inside a ternary?
Because return, raise, and assignment are statements, and a conditional expression can only contain expressions. You can return the result of a ternary — return 0 if plan == free else 19 — but you cannot put the return keyword inside one.
What is the difference between the ternary and using or for a default?
or tests truthiness, so it replaces every falsy value including 0, empty string, and empty list. If those are legitimate values, or silently destroys them. Use x if y is None else y when you specifically mean the None case.
Where does the ternary go in a list comprehension?
Before the for when you are transforming every element, after the for when you are filtering. [a if c else b for x in xs] keeps the same number of elements; [x for x in xs if c] returns fewer. Mixing these up is a common source of confusion.