Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
result = []                    # before
for item in source:            # the source
    if <keep this one>:        # the filter
        result.append(<what>)  # the output
<use result>                   # after

Four 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
['axi_burst', 'reset_seq']

Correct, clear, and four lines of which one is failing = [].

3. The Same Thing, One Line

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
['axi_burst', 'reset_seq']

Every piece of the loop is in there, in a different order. Line them up:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
['axi_burst', 'reset_seq']
['axi_burst', 'reset_seq']
equal: True
same type: True

An 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
['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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
print([r["test"] for r in []])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
[]

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
items = [1, 2, 3]

for n in items:
    pass

print("after the loop, n is:", n)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
after the loop, n is: 3

A comprehension's variable does not:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
items = [1, 2, 3]

doubled = [n * 2 for n in items]

print(doubled)
print(n)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
[2, 4, 6]
NameError: name 'n' is not defined

The 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
n = "untouched"

doubled = [n * 2 for n in [1, 2, 3]]

print(doubled)
print(n)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
[2, 4, 6]
untouched

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
loop  : ['UVM_ERROR', '@1:', 'UVM_INFO', '@2:'] 4

Two 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
comp  : ['UVM_ERROR', 'UVM_INFO'] 2
paired: ['UVM_ERROR @1:', 'UVM_INFO @2:'] 2

Two 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
[  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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
widths = {"clk": 1, "data": 32, "addr": 16}

wide = [name for name in widths if widths[name] > 8]

print(wide)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
['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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
widths = {"clk": 1, "data": 32, "addr": 16}

wide = []
for name in widths:
    if widths[name] > 8:
        wide.append(name)

print(wide)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
['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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 ==:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# (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 if becomes 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 for loop'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.