Filtering
An if at the end keeps only some items:
evens = [n for n in nums if n % 2 == 0]
That is the filter position, and it takes no else. If you want to choose between two values rather than keep or drop, the conditional goes in the expression at the front instead:
labels = ["even" if n % 2 == 0 else "odd" for n in nums]
Two different ifs, in two different places, doing two different jobs. Mixing them up is the most common comprehension error.
Nesting reads outer-first
[n for row in grid for n in row]
The clauses come in the same order as the nested loops would: for row outside, for n inside. It reads oddly because the expression sits before both, but the order after the expression is exactly the loop order.
One level of nesting is defensible. Two, plus a filter, is a line that will cost you a minute every time you come back to it — and the loop version costs nothing. The second program on this page puts both side by side and prints the same answer from each.
Comprehension or generator
Swap the brackets for round ones and you get a generator expression:
total = sum(n * n for n in range(10_000))
The comprehension builds the whole list first — every element in memory — then sums it. The generator produces one value at a time and never builds a list at all. When the result is consumed immediately by sum, any, max or a for, the generator is the better default, and the size difference is measurable: the page prints both.
A worked example, step by step
Take a list of records and pull out the names of everyone who passed:
rows = [{"name": "ana", "score": 91},
{"name": "bo", "score": 43},
{"name": "cy", "score": 68}]
passed = [r["name"] for r in rows if r["score"] >= 50]
Read it in the order it executes, which is not the order it is written: take each r from rows, keep it if the score clears 50, and collect r["name"]. The expression at the front is applied last, to whatever survives the filter.
Written as a loop that is four lines and one more variable to name.
The variable does not leak
squares = [n * n for n in range(5)]
print(n) # NameError
The loop variable belongs to the comprehension and is gone afterwards. That is a deliberate difference from a for statement, whose variable outlives the loop, and it is the reason a comprehension cannot quietly overwrite an n you were using elsewhere.
Conditional expression versus filter, once more
These two look similar and do different jobs:
[n for n in nums if n > 0] # filter: some items are dropped
["pos" if n > 0 else "neg" for n in nums] # map: every item is kept
The first can return a shorter list. The second always returns one item per input. If you find yourself wanting if ... else at the end, you almost certainly want it at the front instead.
Building a dict or a set the same way
The syntax generalises:
{w: len(w) for w in words} # dict comprehension
{len(w) for w in words} # set comprehension
Braces with a colon give a dictionary; braces without give a set. Both take the same trailing if, and both are worth reaching for when the loop version would be three lines of result[key] = value.
What a comprehension is really for
The value of a comprehension is not that it is shorter. It is that it says "this is a transformation" in a way a loop cannot. A loop that builds a list is three separate statements - create, iterate, append - and a reader has to hold all three to work out that nothing else is happening in between. A comprehension states the shape of the result on the first line, and there is nowhere for anything else to hide.
That is also the test for whether you should use one. If the body does one thing to each item, a comprehension expresses it better. If it does several things, updates something outside itself, or needs a try, the loop is not just acceptable but correct, because those are exactly the things a comprehension cannot contain and should not be contorted into.
A note on nesting
A comprehension containing a second for is a nested loop, and it is subject to the same multiplication of work described elsewhere on this site. Flattening a list of lists is a fair use. Comparing every pair of items is not - it is the same quadratic cost written more compactly, and compactness is the last thing that helps when something is slow.
What a comprehension cannot contain
The limits are not arbitrary; they follow from a comprehension being an expression, and knowing them tells you exactly when to stop trying.
No statements. No assignment with =, no return, no raise, no pass. The walrus := is the one exception, added precisely because naming an intermediate value was the one statement people genuinely needed.
**No try.** If one item might raise, a comprehension cannot handle it. The whole thing fails and you lose the items already processed. A loop with a try inside is the only way to skip the bad ones and keep the rest, and that is a common enough requirement that it alone decides the shape of a lot of data-cleaning code.
**No break or continue.** A filter can drop items, but nothing can stop the iteration early. itertools.takewhile covers the break case, and the trailing if covers continue.
Nothing useful for side effects. A comprehension whose result is discarded — [print(x) for x in items] — builds a list of None to throw away, and hides the fact that iteration is the point. Write the loop.
Each of those is a signal rather than an obstacle. When you find yourself wanting one, the answer is not a cleverer comprehension; it is that the job has outgrown the form.
Reading one somebody else wrote
Comprehensions are written left to right and executed in a different order, which is what makes an unfamiliar one hard to parse. A reliable method:
**Find the for clauses first.** Read them left to right; they are the loops, outermost first. Ignore everything before the first for while you do this.
**Then the trailing if.** It filters whatever the loops produced, and it runs before the expression at the front.
Then the expression. It applies last, to each surviving item, and it is what the result contains.
Finally the brackets. Square gives a list, braces a set, braces with a colon a dict, round a generator that has not run yet.
So [f(x) for row in grid for x in row if x] reads as: for each row in grid, for each x in row, keep the truthy ones, and collect f(x). Four clauses, read in the order they execute, which is right to left except for the expression that comes first on the page.
The value of having a method is that it also tells you when a comprehension is too much: if the method takes more than a few seconds, the loop version would have been read in one pass.
The cost, and where the loop wins
A comprehension is usually a little faster than the equivalent loop, because the appending happens in the interpreter's own loop rather than through a method lookup on every pass. The difference is real and small, and it is almost never the deciding factor.
Where the difference does matter is memory. A list comprehension over a large input builds the whole result before anything else happens; a generator expression does not. For a file with millions of lines, that is the difference between a program that runs and one that does not, and it costs one character to switch.
Where the loop genuinely wins is everything the previous section listed, plus one more: debugging. You can put a print or a breakpoint inside a loop body. You cannot put one inside a comprehension without changing it into something else. When a transformation is producing the wrong answer and you do not know why, converting it to a loop for five minutes is often the fastest route to the cause.
The shape it replaces, and the one it does not
A comprehension replaces exactly one loop shape: build an empty collection, iterate, append something derived from each item. Recognising that shape is how you know a comprehension applies without having to try it.
The tell is that the accumulator is only ever appended to, and the thing appended depends only on the current item. Nothing reads the accumulator, no other variable is updated, and the loop body is a single expression.
The shapes it does not replace are worth naming, because they look similar.
Accumulating. A running total, a maximum so far, a count — anything where the next value depends on what came before. Comprehensions have no access to what they have already produced, which is why sum, max and Counter exist as separate tools.
Grouping. Building a dictionary of lists needs to append to an entry that may already exist, which is a read of the accumulator. setdefault or defaultdict in a loop is the answer.
Two outputs. Splitting one input into two lists in a single pass. A comprehension produces one collection, so this is either two comprehensions over the same data or one loop — and the loop reads the input once.
Questions people ask
Is a comprehension faster than a loop? Slightly, for building a list. Not enough to choose one for that reason.
Can I use two filters? Yes, and chained filters mean and.
Why does my nested comprehension have the loops backwards? Because the clauses read outer-first, the same order as nested for statements. It is the guess most people get wrong.
Can I reference the previous item? Not directly. Zip the list with a shifted copy of itself, or write the loop.
Does the loop variable leak? No, a comprehension has its own scope. A for statement does not.
Can I build a list of lists? Yes, with a comprehension in the expression slot: [[f(x) for x in row] for row in grid].
When should I definitely not use one? When the body needs a try, a break, an assignment, or exists for a side effect.
Can a comprehension be spread over several lines? Yes, and it often should be — one clause per line, with the expression on the first, reads far better than a long single line.
Is there a limit to how many for clauses I can use? No limit in the language, and a practical one of about two before it stops being readable.
Why does a generator expression inside a function call need no extra brackets? Because it is the only argument. With more than one argument, the brackets are required to say where it ends.
Should I name the result of a comprehension? Almost always. A comprehension passed straight into another call is compact and gives the reader nothing to hold on to; a name says what the collection is.
Recap in one screen
- A comprehension states the shape of the result first, which is what makes it clearer than a loop for a plain transformation.
- The
for clauses read outermost-first; the trailing filter runs before the expression at the front. - It is an expression, so no statements, no
try, no break — and those limits are the signal to write the loop. - Round brackets give a generator that builds nothing, which is the right default inside
sum, any, max and join. - One
for, one filter: past that, the loop is easier to read, to change and to debug.