Python Part I · Module 12 · Comprehensions
From a Loop to a List Comprehension
Chapter 10.10 established the pattern behind almost every useful loop: create an empty list before it, append inside it, use it afterwards. Written out, that is four lines, three of which are ceremony — the empty list, the append, and the loop header exist to support one idea, which is usually a filter or a transformation. Python has a compact form that expresses the whole thing as a single expression. It has a reputation for being cryptic, and that reputation comes almost entirely from being taught the wrong way round. Learn it as a translation of a loop you already wrote and it stops being syntax to decode.
Intermediate15 min readPythonPart IFundamentalsComprehensionsLists
Part I · Module 12 · Chapter 12.1 · From a Loop to a List Comprehension
1. The Shape You Already Write
Every list-building loop in Module 10 had the same skeleton:
result = [] # before
for item in source: # the source
if <keep this one>: # the filter
result.append(<what>) # the output
<use result> # afterFour lines to say the failing test names. Three of them — the empty list, the loop header, the append — are machinery, and only <what> and <keep this one> carry the idea.
2. The Loop
results = [
{"test": "axi_smoke", "status": "PASS"},
{"test": "axi_burst", "status": "FAIL"},
{"test": "reset_seq", "status": "FAIL"},
]
failing = []
for r in results:
if r["status"] == "FAIL":
failing.append(r["test"])
print(failing)['axi_burst', 'reset_seq']Correct, clear, and four lines of which one is failing = [].
3. The Same Thing, One Line
results = [
{"test": "axi_smoke", "status": "PASS"},
{"test": "axi_burst", "status": "FAIL"},
{"test": "reset_seq", "status": "FAIL"},
]
failing = [r["test"] for r in results if r["status"] == "FAIL"]
print(failing)['axi_burst', 'reset_seq']Every piece of the loop is in there, in a different order. Line them up:
THE LOOP WHERE IT GOES
--------------------------------------- -------------------------
failing = [] gone — the [ ] does this
for r in results: 2nd: for r in results
if r["status"] == "FAIL": 3rd: if r["status"] == "FAIL"
failing.append(r["test"]) 1st: r["test"]
gone — the [ ] does this
failing = [r["test"] for r in results if r["status"] == "FAIL"]
└ 1st ─┘ └──── 2nd ────┘ └──────── 3rd ────────┘- The append argument —
r["test"]— moves to the front. - The loop header —
for r in results— stays in the middle, word for word. - The filter —
if r["status"] == "FAIL"— stays at the end, word for word, minus its colon. - The empty list and the
.append()call disappear entirely. The brackets do that job.
Read it aloud in the order it is written: the test name, for each result, if the status is FAIL. That is an English sentence, and it is why the output goes first.
4. They Really Are the Same
Not an approximation — the same list:
results = [
{"test": "axi_smoke", "status": "PASS"},
{"test": "axi_burst", "status": "FAIL"},
{"test": "reset_seq", "status": "FAIL"},
]
by_loop = []
for r in results:
if r["status"] == "FAIL":
by_loop.append(r["test"])
by_comprehension = [r["test"] for r in results if r["status"] == "FAIL"]
print(by_loop)
print(by_comprehension)
print("equal:", by_loop == by_comprehension)
print("same type:", type(by_loop) is type(by_comprehension))['axi_burst', 'reset_seq']
['axi_burst', 'reset_seq']
equal: True
same type: TrueAn ordinary list, in the same order. Order is preserved because the comprehension visits the source in exactly the order the loop did.
5. The Filter Is Optional
Drop the if and every item contributes:
results = [
{"test": "axi_smoke", "status": "PASS"},
{"test": "axi_burst", "status": "FAIL"},
]
print([r["test"] for r in results])
print([r["status"] for r in results])['axi_smoke', 'axi_burst']
['PASS', 'FAIL']That is the loop without the if — a straight transformation of every item, which is Chapter 12.3's subject.
And an empty source gives an empty result, with no special case, exactly as Chapter 10.1 Section 8 said of loops:
print([r["test"] for r in []])[]6. The One Real Difference: the Variable Does Not Survive
Everything so far has been a translation. Here is the single behavioural difference, and it is worth knowing because it is the opposite of what Module 10 taught.
Chapter 10.1 Section 4 established that a for loop's variable survives the loop, holding the last item:
items = [1, 2, 3]
for n in items:
pass
print("after the loop, n is:", n)after the loop, n is: 3A comprehension's variable does not:
items = [1, 2, 3]
doubled = [n * 2 for n in items]
print(doubled)
print(n)[2, 4, 6]
NameError: name 'n' is not definedThe comprehension has its own namespace — n exists only while it runs and is gone afterwards, exactly like a function's local names in Chapter 11.10.
n = "untouched"
doubled = [n * 2 for n in [1, 2, 3]]
print(doubled)
print(n)[2, 4, 6]
untouchedThe outer n is intact. The identical loop would have left it as 3.
7. Exactly One Output per Kept Item
A comprehension has one output expression, so each item that survives the filter contributes exactly one value. A loop body has no such limit — it can append twice, or not at all:
log = ["UVM_ERROR @1: crc", "UVM_INFO @2: ok"]
pairs = []
for line in log:
pairs.append(line.split()[0])
pairs.append(line.split()[1])
print("loop :", pairs, len(pairs))loop : ['UVM_ERROR', '@1:', 'UVM_INFO', '@2:'] 4Two lines in, four values out. No comprehension can do that, because there is one slot for the output and it is evaluated once per item.
log = ["UVM_ERROR @1: crc", "UVM_INFO @2: ok"]
firsts = [line.split()[0] for line in log]
both = [line.split()[0] + " " + line.split()[1] for line in log]
print("comp :", firsts, len(firsts))
print("paired:", both, len(both))comp : ['UVM_ERROR', 'UVM_INFO'] 2
paired: ['UVM_ERROR @1:', 'UVM_INFO @2:'] 2Two in, two out, both times. The second version gets both fields into the result by making one value that contains them — a combined string here, and a tuple or a dictionary when the parts should stay separate.
8. The Three Parts, Named
From here on these names are used throughout the module:
[ r["test"] for r in results if r["status"] == "FAIL" ]
───────── ─────────────── ────────────────────────
OUTPUT SOURCE FILTER- Output — the expression evaluated once per kept item. What ends up in the list.
- Source —
for <name> in <collection>, identical to a loop header. - Filter — an optional
if, deciding which items reach the output.
They always appear in that order, and the output comes first even though it happens last. That inversion is the only genuinely unusual thing about the syntax.
9. Reading One You Did Not Write
The reliable method is to unfold it — read the clauses right to left, building the loop back:
widths = {"clk": 1, "data": 32, "addr": 16}
wide = [name for name in widths if widths[name] > 8]
print(wide)['data', 'addr']Right to left: filter — keep names whose width is over 8; source — for each name in widths; output — collect the name. Which is:
widths = {"clk": 1, "data": 32, "addr": 16}
wide = []
for name in widths:
if widths[name] > 8:
wide.append(name)
print(wide)['data', 'addr']Note the source: looping a dictionary gives its keys, as Chapter 10.3 established. A comprehension changes nothing about what the source hands you.
10. Predict, Run, Observe
# A
nums = [1, 2, 3, 4]
print("A:", [n * 10 for n in nums])
# B
nums = [1, 2, 3, 4]
print("B:", [n for n in nums if n > 2])
# C
names = ["clk", "rst_n", "data"]
print("C:", [len(name) for name in names])
# D
items = [1, 2]
out = [x for x in items]
print("D:", out, "items unchanged:", items)The verified output:
A: [10, 20, 30, 40]
B: [3, 4]
C: [3, 5, 4]
D: [1, 2] items unchanged: [1, 2]A — no filter, so four items in and four out, each transformed.
B — no transformation; the output expression is just n, so the comprehension is a pure filter.
C — the output is a call, which is fine: it is an ordinary expression.
D — a comprehension reads its source and never modifies it, exactly as a loop does not (Chapter 10.1, Section 7).
11. Exercises
Exercise 1 — fold it. Convert each loop to a comprehension, then check the two results are ==:
# (a)
out = []
for n in [1, 2, 3]:
out.append(n + 1)
# (b)
out = []
for name in ["clk", "rst"]:
if len(name) > 3:
out.append(name)Exercise 2 — unfold it. Write the explicit loop for each: (a) [r["seed"] for r in results] (b) [s for s in statuses if s != "PASS"] (c) [name.upper() for name in names if name.startswith("axi")].
Exercise 3 — name the parts. For [r["test"] for r in results if r["runtime_s"] > 600], identify the output, the source and the filter, and say which one runs first.
Exercise 4 — the variable. Set item = "before", then run a comprehension using item as its variable, then print item. Do the same with a for loop. Explain the two results in two sentences.
Exercise 5 — no leak. Write a comprehension and then try to use its variable afterwards. Record the error. Say in one sentence why this is better than a loop's behaviour.
Exercise 6 — empty source. Predict the result of a comprehension over an empty list, and of one over an empty dictionary. Verify both and say what guard you did not need to write.
Exercise 7 — the dictionary source. For widths = {"clk": 1, "data": 32}, predict [k for k in widths] and [widths[k] for k in widths], then verify. Which chapter's rule decided the first one?
Exercise 8 — read it aloud. Write the English sentence for each: (a) [r["test"] for r in results] (b) [n for n in seeds if n % 2 == 0] (c) [line.strip() for line in log if line.strip()].
12. Summary
- A list comprehension is a translation of an accumulator loop, not a different idea — learn it by folding a loop you already wrote.
- Every piece has somewhere to go: the append argument becomes the output and moves to the front, the loop header becomes the source, the
ifbecomes the filter. - The empty list and the
.append()disappear. The brackets do that work. - The result is an ordinary list, equal to the loop's and in the same order.
- The filter is optional; without it every item contributes.
- An empty source gives an empty result, with no guard needed.
- The comprehension's variable does not survive it — unlike a
forloop's, which holds the last item. It has its own namespace, like a function. - That means it cannot clobber an outer name of the same spelling, and cannot be misread afterwards.
- A comprehension emits exactly one value per kept item, so the result can never be longer than the source — which is what makes the count a reliable diagnostic.
- The three parts are output, source, filter, always in that order, with the output written first even though it is evaluated last.
- To read one, unfold it right to left back into the loop.
The next chapter builds the other two collections the same way.
Where this fits
Part of the Python curriculum.
