Python Part I · Module 12 · Comprehensions
Conditional Expressions Inside Comprehensions
One keyword appears in two places inside a comprehension and means two completely different things. At the end it is a filter, and it decides whether an item appears in the result at all. At the front it is a conditional expression, and it decides what an item turns into while leaving the count alone. They look similar enough that experienced engineers mix them up, and the usual symptom is not an error — it is a result with the wrong number of rows in it. This chapter separates them with one pair of programs and one number, and that number is the reliable way to tell which one you have written.
Intermediate15 min readPythonPart IFundamentalsComprehensionsConditionals
Part I · Module 12 · Chapter 12.4 · Conditional Expressions Inside Comprehensions
1. Two Things Named if
You have met both already, in different chapters.
The filter clause — Chapter 12.1, Section 7. It sits after the source and decides which items reach the output:
[r["test"] for r in results if r["status"] == "FAIL"]
─────────────────────
the filterThe conditional expression — Chapter 9.6. It is an expression that produces one of two values:
"PASS" if errors == 0 else "FAIL"Put the second inside a comprehension and the result is a line with two ifs that mean unrelated things. Keeping them apart is this chapter's whole job.
2. The Data
results = [
{"test": "axi_smoke", "status": "PASS"},
{"test": "axi_burst", "status": "FAIL"},
{"test": "reset_seq", "status": "PASS"},
]
print(len(results))3Three results, two of which passed.
3. The Pair That Settles It
A filter — the if is at the end:
results = [
{"test": "axi_smoke", "status": "PASS"},
{"test": "axi_burst", "status": "FAIL"},
{"test": "reset_seq", "status": "PASS"},
]
passed = [r["test"] for r in results if r["status"] == "PASS"]
print(passed)
print(len(passed), "of", len(results))['axi_smoke', 'reset_seq']
2 of 3A conditional expression — the if is at the front, and there is an else:
results = [
{"test": "axi_smoke", "status": "PASS"},
{"test": "axi_burst", "status": "FAIL"},
{"test": "reset_seq", "status": "PASS"},
]
verdicts = ["ok" if r["status"] == "PASS" else "investigate" for r in results]
print(verdicts)
print(len(verdicts), "of", len(results))['ok', 'investigate', 'ok']
3 of 3Two of three against three of three. Same source, same condition, same keyword — and one answers which tests passed while the other answers what to do about each test.
4. Position Is the Rule
[ "ok" if passed else "bad" for r in results if r["status"] == "FAIL" ]
────────────────────────── ──────────────── ────────────────────────
CONDITIONAL EXPRESSION SOURCE FILTER
(the output) (drops items)- Before the
for— part of the output expression. It must have anelse, because Chapter 9.6 established that an expression must produce a value on every path. - After the source — a filter clause. It must not have an
else, because a clause is not an expression and there is nothing for anelseto produce.
That is the entire distinction, and it is positional rather than semantic. The same words in the two slots do different jobs.
| Where | else | Changes the count | What it does | |
|---|---|---|---|---|
| conditional expression | before for | required | no | picks one of two values |
| filter clause | after the source | not allowed | yes | drops items |
5. The Mistake Everyone Makes
Trying to put an else on the filter:
results = [{"test": "a", "status": "PASS"}]
labels = [r["test"] for r in results if r["status"] == "PASS" else "skipped"]SyntaxError: invalid syntaxPython refuses before anything runs. A filter clause takes no else — there is no value for it to produce, because a filter does not produce values at all. It answers yes or no about an item the output expression has already been written for.
If what you wanted was a label for every item, which differs by status, the condition belongs at the front:
results = [
{"test": "a", "status": "PASS"},
{"test": "b", "status": "FAIL"},
]
labels = [r["test"] if r["status"] == "PASS" else "skipped" for r in results]
print(labels)['a', 'skipped']Two items in, two out — the failing one became "skipped" rather than disappearing.
6. Both in One Comprehension
They occupy different slots, so they can coexist:
results = [
{"test": "axi_smoke", "status": "PASS", "sim": "vcs"},
{"test": "axi_burst", "status": "FAIL", "sim": "vcs"},
{"test": "reset_seq", "status": "PASS", "sim": "xrun"},
]
vcs_verdicts = ["ok" if r["status"] == "PASS" else "investigate"
for r in results
if r["sim"] == "vcs"]
print(vcs_verdicts)
print(len(vcs_verdicts), "of", len(results))['ok', 'investigate']
2 of 3Read it in two passes. The filter cut three results to the two run under vcs. The conditional expression then turned each of those two into a word. One clause changed the count; the other changed the contents.
Written across three lines as above, with the source and the filter on their own, it stays readable. On one line it would not, which is Section 7's point.
7. When to Stop
A conditional expression in a comprehension is one condition's worth of complexity on top of a construct that already has three parts. Two guidelines, both from chapters you have:
Do not nest conditional expressions. Chapter 9.6 Section 7 ruled this out on its own; inside a comprehension it is worse:
errors = [0, 5, 50]
bands = ["clean" if n == 0 else "minor" if n < 10 else "major" for n in errors]
print(bands)['clean', 'minor', 'major']That is correct and it is at the limit. A third level would need re-parsing from the middle, and the answer there is a named function — Chapter 11.2's — called from the output expression:
def band(n):
"""Return the severity band for an error count."""
if n == 0:
return "clean"
if n < 10:
return "minor"
if n < 100:
return "major"
return "critical"
errors = [0, 5, 50, 500]
print([band(n) for n in errors])['clean', 'minor', 'major', 'critical']The comprehension is now one short line that says what it does, the branching lives somewhere it can have a docstring, and band can be checked on its own — Chapter 11.15's whole argument.
Break the line when both forms appear. The three-line layout in Section 6 is not a formatting preference; a filter and a conditional expression on one line put two different ifs within a few characters of each other, which is the exact confusion this chapter exists to prevent.
8. Predict, Run, Observe
nums = [1, 2, 3, 4]
# A
print("A:", [n for n in nums if n % 2 == 0])
# B
print("B:", ["even" if n % 2 == 0 else "odd" for n in nums])
# C
print("C:", [n * 10 if n % 2 == 0 else n for n in nums])
# D
a = [n for n in nums if n > 2]
b = ["big" if n > 2 else "small" for n in nums]
print("D:", len(a), len(b))The verified output:
A: [2, 4]
B: ['odd', 'even', 'odd', 'even']
C: [1, 20, 3, 40]
D: 2 4A — a filter: two of four.
B — a conditional expression: four of four, each replaced by a word.
C — a conditional expression where one branch transforms and the other passes the item through unchanged. Still four items; the odd ones were kept as they were.
D — the two lengths side by side. 2 and 4 from the same source is the whole chapter.
9. Exercises
Exercise 1 — which is it. For each, say filter or conditional expression, and give the output length for a four-item source: (a) [x for x in xs if x > 0] (b) [x if x > 0 else 0 for x in xs] (c) [abs(x) for x in xs] (d) [x for x in xs if x].
Exercise 2 — the pair. Using a three-result list, write both the filter version and the conditional-expression version of "which tests passed". Print both results and both lengths, and say which answers which question.
Exercise 3 — the SyntaxError. Write a filter with an else on it and record the exact message. Explain in two sentences why Python cannot accept it.
Exercise 4 — move it to the front. Take your Exercise 3 line and rewrite it so every item produces a value, with failures becoming "skipped". Confirm the length now equals the source's.
Exercise 5 — both. Write one comprehension that keeps only the xrun runs and labels each as "ok" or "investigate". Lay it out across three lines and say why.
Exercise 6 — clamp. Given a list of runtimes, build a list where anything over 600 becomes exactly 600 and everything else is unchanged. Say which form you used and why the count did not change.
Exercise 7 — past the limit. Write a three-level nested conditional expression inside a comprehension, then rewrite it with a named function. Say what the function version gained besides length.
Exercise 8 — read the count. A colleague's comprehension over 40 results returns 40 items. Without seeing it, what can you conclude about its if? What if it returned 31?
10. Summary
- Two different things are spelled
ifinside a comprehension, and position tells them apart. - Before the
forit is a conditional expression — part of the output, requires anelse, picks one of two values. - After the source it is a filter clause — no
elseallowed, decides whether an item appears at all. - A filter changes the count; a conditional expression never does. That is the reliable discriminator.
- An
elseon a filter is aSyntaxError, caught before anything runs — the language enforcing the positional rule. - Both can appear in one comprehension, because they occupy different slots.
- Lay that version out across lines. Two unrelated
ifs on one line is the confusion this chapter exists to prevent. - Do not nest conditional expressions — two levels is the limit, and past it a named function is the answer.
- A named function in the output expression keeps the comprehension short, gives the branching a docstring, and makes it checkable on its own.
The next chapter adds the third collection type, and the one job it does better than either of the others.
Where this fits
Part of the Python curriculum.
