Senior 6 min · March 05, 2026

Python List Comprehensions — The 500K Email OOM Crash

Server OOM from a list comprehension building 500K emails — understand the memory trap and use for loops instead.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • List comprehensions build new lists by applying an expression to each item in an iterable, optionally filtered.
  • Syntax: [expression for item in iterable if condition] — read it left to right: "give me expression, for each item in iterable, but only if condition".
  • Filtering uses a trailing if; transforming uses a ternary inside the expression. Mix them up and you'll get wrong results silently.
  • Performance: ~10–35% faster than a for loop with .append(), but only for large lists — the speedup comes from avoiding method lookup overhead.
  • Production trap: using a list comprehension for side effects (printing, DB writes) builds a thrown-away list and confuses the next dev. Use a plain loop instead.
Plain-English First

Imagine you're at a fruit stall and you want to pick only the ripe apples, wash each one, and put them in a bag — all in one smooth motion. A list comprehension is exactly that: a single instruction that loops through a collection, optionally filters items, and transforms each one into a new list. Instead of writing three separate steps (loop, check, append), you describe the whole operation in plain English-like code on one line. It's not magic — it's just a more natural way to say 'give me this, from that, if this condition is true'.

Every Python program that works with data — scraping websites, processing CSVs, filtering API responses — spends a huge amount of time building new lists from old ones. How you do that has a direct impact on how readable your code is and, to a meaningful degree, how fast it runs. List comprehensions are Python's built-in answer to this everyday problem, and they're one of the first things experienced Python developers reach for when they see a for-loop that builds a list.

Before list comprehensions existed, building a filtered or transformed list meant writing a loop, declaring an empty list, and calling .append() on every iteration — four to six lines of boilerplate just to express a single idea. That ceremony buries the actual intent of the code under a pile of scaffolding. List comprehensions collapse all of that into one expression that reads almost like a sentence, making your intent immediately obvious to the next developer — or to yourself six months later.

By the end of this article you'll know exactly how list comprehensions work under the hood, when they're the right tool and when they're not, how to layer in filtering and nesting without creating unreadable one-liners, and the two or three mistakes that trip up almost every developer the first time they use them in a real project. You'll also walk away with concrete answers to the interview questions that come up every time this topic surfaces.

The Anatomy of a List Comprehension — Reading It Like a Sentence

A list comprehension has three parts, and the order they sit in the expression mirrors the order you'd describe them out loud. The structure is: [expression for item in iterable if condition]. Read it left to right and it says: 'Give me expression, for each item in iterable, but only if condition is true.' The if clause is completely optional — leave it out and every item gets transformed.

The reason the expression comes first — before the for — is that it puts the most important thing front and centre. You're telling the reader immediately what each element of the new list will look like. The loop mechanics and the filter are supporting details that follow.

Under the hood, Python compiles a list comprehension into bytecode that's slightly faster than an equivalent for loop with .append(). That's because .append() has to look up the method on the list object every single iteration, whereas the comprehension's internal C-level loop skips that lookup. For small lists the difference is negligible, but at tens of thousands of items it starts to matter.

One crucial mental model: a list comprehension always produces a brand-new list. It never mutates the original. If you're iterating over temperatures and writing [t * 1.8 + 32 for t in temperatures], your original temperatures list is completely untouched.

list_comprehension_basics.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# ── Basic transformation: convert Celsius readings to Fahrenheit ──
celsius_readings = [0, 20, 37, 100]

# Traditional loop approach — 4 lines to say one thing
fahrenheit_loop = []
for temp in celsius_readings:
    fahrenheit_loop.append(temp * 1.8 + 32)

# List comprehension — same result, one line, reads like English
# 'Give me (temp * 1.8 + 32) for each temp in celsius_readings'
fahrenheit_comp = [temp * 1.8 + 32 for temp in celsius_readings]

print("Loop result:  ", fahrenheit_loop)
print("Comprehension:", fahrenheit_comp)
print("Same output?  ", fahrenheit_loop == fahrenheit_comp)

# ── Adding a filter: only convert temps above freezing ──
above_freezing_f = [temp * 1.8 + 32 for temp in celsius_readings if temp > 0]
print("Above freezing:", above_freezing_f)
Output
Loop result: [32.0, 68.0, 98.6, 212.0]
Comprehension: [32.0, 68.0, 98.6, 212.0]
Same output? True
Above freezing: [68.0, 98.6, 212.0]
Read It Backwards to Understand It
When a comprehension looks confusing, read it from right to left: start with the iterable, then the filter, then ask 'what happens to each surviving item?' That order matches how Python actually evaluates it.
Production Insight
In production code, reading comprehension backwards becomes second nature after a few reviews.
The real trap is not the syntax — it's assuming the comprehension doesn't have side effects.
If the expression calls a function that writes to disk, you've just made I/O invisible.
Key Takeaway
List comprehensions are faster and more readable than loops for simple transformations.
But they build a full list in memory and hide complexity behind a one-liner.
Rule: if the expression is a function call that has side effects, you're doing it wrong.
When to Use a List Comprehension vs. a Loop
IfGoal is to build a new list from existing data
UseUse list comprehension if the logic fits in 2 lines
IfLogic has multiple conditions, nested loops, or side effects
UseUse a regular for loop for clarity and debuggability
IfProcessing a large dataset and only need to iterate once
UseUse a generator expression (lazy), not a list comprehension

Filtering with Conditions — The if Clause That Changes Everything

The if clause at the end of a comprehension is a gate: only items that pass the test make it into the output list. This is where list comprehensions really start to earn their keep in real-world code, because filtering and transforming at the same time is something you do constantly — think 'get me all active users and format their names' or 'find all log lines that contain an error and strip the timestamp'.

There's an important distinction to keep straight: the if at the end of the comprehension (after the for) is a filter — it controls which items are included. A conditional expression inside the output expression (using value_if_true if condition else value_if_false) is a transformation — it changes what an item becomes. You can use both in the same comprehension, and knowing which is which prevents a lot of confusing bugs.

For example, [score if score >= 50 else 0 for score in exam_scores] transforms every failing score to zero but keeps passing scores as-is. Compare that to [score for score in exam_scores if score >= 50] which simply drops failing scores entirely. The first changes items; the second removes them. These are fundamentally different operations, and mixing them up produces wrong results silently — Python won't complain either way.

list_comprehension_filtering.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
exam_scores = [72, 45, 88, 31, 95, 50, 60, 29]

# ── Filter only: remove failing scores (below 50) from the list ──
passing_scores = [score for score in exam_scores if score >= 50]
print("Passing scores:", passing_scores)

# ── Transform only: convert to letter grades using inline conditional ──
# The ternary expression is the OUTPUT, not a filter — every item survives
letter_grades = [
    "A" if score >= 90
    else "B" if score >= 75
    else "C" if score >= 60
    else "D" if score >= 50
    else "F"
    for score in exam_scores
]
print("Letter grades:", letter_grades)

# ── Combined: filter AND transform in one expression ──
# Only passing scores, and map them to 'Pass: <score>'
passing_labelled = [
    f"Pass: {score}"          # transformation applied to survivors
    for score in exam_scores
    if score >= 50             # only scores that pass this gate get transformed
]
print("Labelled passes:", passing_labelled)

# ── Real-world pattern: clean a list of raw strings from user input ──
raw_tags = [" python ", "  ", "Django", "", " REST api "]
cleaned_tags = [
    tag.strip().lower()        # strip whitespace and normalise case
    for tag in raw_tags
    if tag.strip()             # filter out blank or whitespace-only strings
]
print("Cleaned tags:", cleaned_tags)
Output
Passing scores: [72, 88, 95, 50, 60]
Letter grades: ['C', 'F', 'B', 'F', 'A', 'D', 'C', 'F']
Labelled passes: ['Pass: 72', 'Pass: 88', 'Pass: 95', 'Pass: 50', 'Pass: 60']
Cleaned tags: ['python', 'django', 'rest api']
Watch Out: Filter vs. Transform Confusion
[x if x > 0 for x in numbers] is a SyntaxError — a ternary expression always needs an else. Write [x if x > 0 else 0 for x in numbers] to transform, or [x for x in numbers if x > 0] to filter. They do different things.
Production Insight
We once debugged a data pipeline where 'zero' values disappeared because someone used [x for x in data if x] instead of [x for x in data if x is not None].
The filter stripped zero (falsy) records, causing financial reports to be off by hundreds of thousands.
Always be explicit about what you're filtering out — falsy vs. None vs. a sentinel.
Key Takeaway
A trailing if excludes items; a ternary in the expression changes values.
They are not interchangeable — Python won't catch the semantic difference.
Rule: when in doubt, write both halves as a nested comprehension for clarity.
Filter vs. Transform Decision
IfYou want to exclude items from the result
UseUse an if clause at the end: [x for x in iterable if condition]
IfYou want to keep all items but change their value
UseUse a ternary inside the expression: [x if cond else y for x in iterable]
IfYou want both filter and transform on the same iteration
UseCombine both: [f(x) for x in iterable if cond]

Nested Comprehensions and Real-World Data — When One Loop Isn't Enough

Sometimes your data isn't a flat list — it's a list of lists. Think a spreadsheet (rows of rows), a game board, or a JSON response that returns a list of orders, each containing a list of items. Nested list comprehensions let you flatten or transform these structures without resorting to nested loops that take up half a screen.

The mental model for reading nested comprehensions is the same 'right to left' trick, but applied twice. In [cell for row in grid for cell in row], you read it as: 'for each row in grid, for each cell in that row, give me cell.' The outermost loop always comes first after the expression.

That said, nesting deeper than two levels is almost always a code smell. If you find yourself writing three for clauses inside one comprehension, stop and ask whether a helper function or a regular loop would be clearer. Readability is the whole point — a comprehension that requires five minutes to decode has failed at its one job.

A genuinely common real-world use case is flattening API response data: an endpoint returns paginated results as a list of pages, each page containing a list of records, and you need one flat list to work with. A two-level comprehension handles this in one expressive line.

list_comprehension_nested.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# ── Flattening a matrix (list of lists) ──
game_board = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

# Read: 'give me cell, for each row in game_board, for each cell in that row'
all_cells = [cell for row in game_board for cell in row]
print("Flattened board:", all_cells)

# ── Real-world: flatten paginated API results ──
# Simulate three pages of user records returned by an API
paginated_users = [
    [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}],
    [{"id": 3, "name": "Carol"}],
    [{"id": 4, "name": "Dave"}, {"id": 5, "name": "Eve"}],
]

# Flatten all pages into a single list and extract just the names
all_user_names = [
    user["name"]              # extract the 'name' field from each user dict
    for page in paginated_users   # outer loop: iterate over pages
    for user in page              # inner loop: iterate over users on each page
]
print("All user names:", all_user_names)

# ── Generating a multiplication table as a 2D list ──
# This builds a list of lists — comprehension produces a list,
# and the expression is itself a comprehension
multiplication_table = [
    [row * col for col in range(1, 6)]   # inner comprehension: one row
    for row in range(1, 6)               # outer comprehension: iterate over rows
]

for row in multiplication_table:
    print(row)
Output
Flattened board: [1, 2, 3, 4, 5, 6, 7, 8, 9]
All user names: ['Alice', 'Bob', 'Carol', 'Dave', 'Eve']
[1, 2, 3, 4, 5]
[2, 4, 6, 8, 10]
[3, 6, 9, 12, 15]
[4, 8, 12, 16, 20]
[5, 10, 15, 20, 25]
Nested ≠ 2D List Comprehension
[f(x) for row in matrix for x in row] flattens into a 1D list. [[f(x) for x in row] for row in matrix] preserves the 2D structure. The position of the inner brackets makes all the difference — the outer expression determines the shape of the result.
Production Insight
A data engineer once flattened a 3-level nested JSON with a comprehension containing three for clauses.
It worked, but no one could read it — including the author a month later.
They replaced it with a regular loop and gained maintainability without losing performance.
Rule: if a nested comprehension doesn't fit on two lines, extract a helper function.
Key Takeaway
Nested comprehensions are great for two-level flattening.
Beyond that, they hurt readability more than they help.
Rule: two for clauses max, or switch to a loop.
Nesting Level Decision
IfFlatten 2-level nesting (e.g., list of lists)
UseUse a two-level comprehension: [x for row in data for x in row]
IfNesting 3+ levels or complex transformation
UseUse a regular loop or decompose into multiple steps
IfNeed to preserve structure (2D output)
UseUse double comprehension with inner brackets: [[f(x) for x in row] for row in data]

When NOT to Use a List Comprehension — Knowing the Limits

List comprehensions have a superpower, but like all superpowers, using them in the wrong situation creates problems. The most important rule is this: if the comprehension doesn't fit on two lines and still read clearly, it's time to switch to a regular loop.

The other big consideration is memory. A list comprehension always builds the entire list in memory immediately. If you're working with a million records and only need to consume them one at a time — say, writing them to a file line by line — you should use a generator expression instead. The syntax is identical, but with round brackets instead of square ones: (expression for item in iterable). A generator is lazy: it produces one item at a time on demand and holds almost nothing in memory at once.

There's also a subtler reason to avoid comprehensions: side effects. If the main purpose of your loop is to do something — print to the console, update a database, send a request — rather than to produce a value, a comprehension is the wrong tool. Using a comprehension purely for its side effects, and throwing away the resulting list, is a code smell that confuses readers and wastes memory.

Finally, never use a list comprehension when a built-in function already does the job more clearly. sum(), max(), filter(), and map() all exist precisely for common cases. Knowing when to reach for them instead is what separates intermediate from advanced Python.

list_comprehension_vs_alternatives.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import sys

product_prices = [12.99, 5.49, 89.00, 3.25, 45.50, 7.80]

# ── List comprehension: fine when you need the full list in memory ──
discounted_prices = [price * 0.9 for price in product_prices]
print("Discounted list:", discounted_prices)
print("Memory (list):", sys.getsizeof(discounted_prices), "bytes")

# ── Generator expression: use when you only need to iterate once ──
# Identical syntax, but with () instead of []
# Nothing is computed until you actually iterate
discounted_gen = (price * 0.9 for price in product_prices)
print("Memory (generator):", sys.getsizeof(discounted_gen), "bytes")

# Consume the generator once — after this it's exhausted
total_discounted = sum(discounted_gen)
print("Total after discount: $", round(total_discounted, 2))

# ── Anti-pattern: comprehension used purely for side effects ──
# This works but is misleading — it builds a list nobody uses
# BAD: [print(price) for price in product_prices]  # don't do this

# GOOD: use a regular for loop when you're doing work, not building a list
print("\n--- Price List ---")
for price in product_prices:
    print(f"  ${price:.2f}")  # side effect (printing) — loop is the right tool

# ── When a built-in is clearer than a comprehension ──
high_value_items = list(filter(lambda p: p > 10, product_prices))
print("\nHigh value (filter):", high_value_items)

# Or with a comprehension — equally readable here, your call
high_value_comp = [p for p in product_prices if p > 10]
print("High value (comp): ", high_value_comp)
Output
Discounted list: [11.691, 4.941, 80.1, 2.925, 40.95, 7.02]
Memory (list): 152 bytes
Memory (generator): 112 bytes
Total after discount: $ 147.63
--- Price List ---
$12.99
$5.49
$89.00
$3.25
$45.50
$7.80
High value (filter): [12.99, 89.0, 45.5]
High value (comp): [12.99, 89.0, 45.5]
The One-Breath Rule
If you can't read the comprehension out loud in one breath and have it make sense, rewrite it as a loop. Cleverness that requires deciphering is a bug waiting to happen. Python's style guide (PEP 8) explicitly recommends keeping comprehensions short.
Production Insight
In a real incident, a team used [send_email(user) for user in users] to send 500k emails.
The comprehension built a list of 500k return values in memory, causing an OOM crash.
The fix: change to a simple for loop and use batching.
Rule: never use a comprehension for side effects — loops are for doing, comprehensions are for collecting.
Key Takeaway
List comprehensions are for building lists, not for running loops with side effects.
Generator expressions save memory when you only need to iterate once.
Rule: if the comprehension does more than produce a new list, you're using the wrong tool.
List Comprehension vs Generator Expression vs Loop
IfYou need the full list in memory (e.g., random access, multiple iterations)
UseUse list comprehension
IfYou only need to iterate once (e.g., sum, write to file)
UseUse generator expression for memory efficiency
IfMain goal is side effects (print, DB write, API call)
UseUse a regular for loop

Performance Deep Dive — When Comprehension Speed Actually Matters

The common wisdom says list comprehensions are ~10–35% faster than for loops with .append(). That's true, but the real question is: does that speedup matter in your use case? For small lists (a few hundred items), the difference is microseconds — not worth sacrificing readability. For large lists (hundreds of thousands or millions), the difference can be seconds, which might matter in a latency-sensitive pipeline.

Where comprehensions really shine is in data processing scripts, API response cleaning, and batch transformations. But be aware: if your comprehension calls a function that does I/O (database, file, network), the I/O cost will completely dominate — the comprehension's speedup becomes irrelevant.

There's another hidden cost: error handling. If a comprehension raises an exception, you lose all context — you can't easily tell which item caused it. In a for loop, you can wrap the transformation in a try/except and log the offending data. Debugging a comprehension that crashes in production often requires rewriting it as a loop just to add logging.

Also, be careful with heavily nested comprehensions. Each level of nesting adds overhead from multiple C loops. A two-level comprehension with a filter and a ternary may still be faster than a nested loop, but profile before you commit.

A practical tip: for numerical data, consider using NumPy instead of a list comprehension. A NumPy array operation is written in C and runs orders of magnitude faster than any Python-level comprehension. np.square(arr) vs [x**2 for x in arr] — the NumPy version is 10-50x faster on large arrays.

list_comprehension_performance.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import timeit

# ── Benchmark: list comprehension vs for loop ──
setup = '''
data = list(range(10000))
'''

comprehension = '''
result = [x * 2 for x in data]
'''

loop = '''
result = []
for x in data:
    result.append(x * 2)
'''

comp_time = timeit.timeit(comprehension, setup, number=1000)
loop_time = timeit.timeit(loop, setup, number=1000)

print(f"Comprehension: {comp_time:.4f}s")
print(f"For loop:      {loop_time:.4f}s")
print(f"Speedup:       {loop_time/comp_time:.2f}x")

# ── The real trap: comprehension that calls a function ──
def transform(x):
    return x * 2  # simulate work

comprehension_func = '''
result = [transform(x) for x in data]
'''

loop_func = '''
result = []
for x in data:
    result.append(transform(x))
'''

comp_func_time = timeit.timeit(comprehension_func, setup + '\nfrom __main__ import transform', number=1000)
loop_func_time = timeit.timeit(loop_func, setup + '\nfrom __main__ import transform', number=1000)

print(f"\nWith function call:")
print(f"Comprehension: {comp_func_time:.4f}s")
print(f"For loop:      {loop_func_time:.4f}s")
print(f"Speedup:       {loop_func_time/comp_func_time:.2f}x")
Output
Comprehension: 0.4512s
For loop: 0.6321s
Speedup: 1.40x
With function call:
Comprehension: 0.8910s
For loop: 1.0234s
Speedup: 1.15x
Profile Before Optimising
Don't blindly replace loops with comprehensions for speed. Profile with timeit or cProfile. The speedup varies by context and is often negligible compared to I/O or function call overhead.
Production Insight
We had a pipeline that processed 10 million log lines. The comprehension was 30% faster than a loop, but both took ~8 seconds — too slow. The real fix was using a generator and streaming to disk, not micro-optimising the loop. Rule: algorithm improvement beats micro-optimisation every time.
Key Takeaway
List comprehensions are faster, but the speedup only matters for large, CPU-bound loops.
Function calls inside a comprehension erode the performance gain.
Rule: optimise for readability first, then profile before rewriting for speed.
When to Care About Comprehension Performance
IfData size < 10,000 items and I/O heavy
UseNegligible difference — write for readability
IfData size > 100,000 items, CPU-bound, pure Python
UseUse comprehension (or NumPy for numeric data)
IfComprehension includes I/O or function calls
UseSpeedup minimal — focus on algorithmic efficiency
● Production incidentPOST-MORTEMseverity: high

List Comprehension Built 500,000 Emails — Then Crashed the Server

Symptom
Server OOM (Out of Memory) error during a batch email campaign. CPU spiked to 100%, workers hung, and the app became unresponsive.
Assumption
List comprehensions are efficient and faster than loops, so using one for sending emails must be fine. The code was clean and compact.
Root cause
[send_email(user) for user in users] builds a list of 500,000 return values (probably None or a status object) in memory before discarding it. The real memory cost was not the result list but the fact that send_email() opened connections and allocated buffers for each call — all held until the comprehension completed. The comprehension also blocked the event loop (if async) because it's eager.
Fix
Replace the comprehension with a simple for loop. Or use a generator expression and iterate over it with for status in (send_email(u) for u in users): but the core fix was to not use a comprehension for side effects. The team also added batching and connection pooling.
Key lesson
  • Never use a list comprehension purely for side effects. It builds a list you don't need and wastes memory.
  • If the primary goal is to do something (send, write, print), reach for a for loop. Comprehensions are for producing new collections.
  • Generator expressions are a middle ground — they're lazy and don't build a list, but they still signal 'iteration, not side effect' poorly.
Production debug guideSymptom → Action guide for real-world comprehension issues3 entries
Symptom · 01
Comprehension raises NameError saying a variable is undefined
Fix
Check loop order in nested comprehensions. The outer loop variable must appear before the inner loop. [x for row in matrix for x in row] is correct; [x for x in row for row in matrix] fails because row is referenced before assignment.
Symptom · 02
Comprehension is slower than expected on a large dataset
Fix
Profile with timeit. If the comprehension includes a function call (f(x) for x in data), the function overhead dominates. Inline the logic or use a vectorised library (NumPy) for numeric data.
Symptom · 03
Comprehension produces unexpected None values in output
Fix
Check if the expression includes a method that returns None (e.g., .append() in a comprehension inside a comprehension). Replace with a proper expression or use a filter to drop None.
★ Quick Debug Cheat Sheet for List ComprehensionsCommand-line snippets to diagnose and fix common list comprehension problems in Python.
NameError in nested comprehension
Immediate action
Swap the order of the `for` clauses to match the outer→inner loop nesting.
Commands
python -c "matrix = [[1,2],[3,4]]; print([x for row in matrix for x in row])"
python -c "matrix = [[1,2],[3,4]]; print([x for x in row for row in matrix])" # fails
Fix now
Rewrite comprehension: [x for row in matrix for x in row]
Comprehension consumes too much memory+
Immediate action
Check if you really need the whole list at once. Use a generator expression instead.
Commands
python -c "print(sum(x**2 for x in range(10**7)))"
python -c "print(sum([x**2 for x in range(10**7)]))"
Fix now
Replace [] with () to make a generator: (x2 for x in range(107))
Comprehension returns `SyntaxError` on a conditional expression+
Immediate action
Add an `else` branch to the ternary inside the expression, or move the condition to the filter position.
Commands
python -c "[x if x > 0 for x in [-1,2]]" # Error
python -c "[x if x > 0 else 0 for x in [-1,2]]" # ok
Fix now
Use [x for x in iterable if condition] for filtering, not ternary.
List Comprehension vs for Loop vs Generator Expression
AspectList Comprehensionfor Loop with .append()Generator Expression
Readability (simple case)Excellent — reads like a sentenceVerbose — intent buried in boilerplateGood — same syntax, different brackets
Readability (complex logic)Can become unreadable fastStays clear as complexity growsSame as comprehension
Performance (CPU-bound)~10–35% faster than for loopSlightly slower due to method lookup overheadSimilar to comprehension but lazy
Memory usageBuilds entire list in RAM immediatelySame — builds entire list in RAM immediatelyLazy — holds no extra memory (just one item at a time)
Side effects (printing, I/O)Wrong tool — antipatternCorrect tool — loops are for doing thingsAlso wrong tool — still signals 'collection building'
Lazy evaluationNot supported — eagerNot supported — eagerLazy — ideal for large datasets
DebuggabilityHarder — can't set breakpoints mid-expressionEasy — breakpoint anywhere inside the loopHarder — same issue as comprehension
Multiple output listsOne comprehension, one listCan build multiple lists simultaneouslyOne generator, one output stream

Key takeaways

1
The output expression comes first in a list comprehension because it's the most important part
what each element becomes. The loop and filter are supporting details.
2
A trailing if filters which items survive; a ternary value_if_true if cond else value_if_false in the expression transforms what surviving items become. These are different operations and can be combined in the same comprehension.
3
Switch to a generator expression (expr for item in iterable) any time you're processing a large dataset you only need to iterate once
it produces values lazily and holds nothing extra in memory.
4
Never use a list comprehension for side effects. If your loop's purpose is printing, writing to a database, or sending requests, a regular for loop is clearer, more debuggable, and the right tool for the job.
5
Nested comprehensions save space but cost readability beyond two levels. The one-breath rule
if you can't read it out loud in one breath, rewrite it as a loop.

Common mistakes to avoid

3 patterns
×

Ternary inside expression without else branch

Symptom
[x if x > 0 for x in numbers] raises SyntaxError. Python can't parse the ternary because it expects an else.
Fix
Add an else branch: [x if x > 0 else 0 for x in numbers]. If you want to drop items, use a filter: [x for x in numbers if x > 0].
×

Using comprehension for side effects

Symptom
Code like [send_email(user) for user in users] works but builds a list of return values that's immediately discarded. Memory waste and confusing intent.
Fix
Use a plain for loop: for user in users: send_email(user). If the function returns nothing, a loop is clearer and doesn't allocate an unused list.
×

Reversing loop order in nested comprehensions

Symptom
Writing [x for x in row for row in matrix] raises NameError because row is referenced before it's defined. The outer loop must come first.
Fix
Write the outer loop first, just as you would in nested for statements: [x for row in matrix for x in row].
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What's the difference between a list comprehension and a generator expre...
Q02SENIOR
Can you rewrite this nested for-loop as a list comprehension? [Interview...
Q03SENIOR
A colleague wrote `[process(record) for record in database_records]` to ...
Q01 of 03SENIOR

What's the difference between a list comprehension and a generator expression, and how do you decide which one to use for a given problem?

ANSWER
A list comprehension builds the entire list in memory immediately, while a generator expression produces items lazily one at a time. Use a list comprehension when you need random access to all elements, or you need to iterate multiple times. Use a generator expression when processing large datasets where you only need to iterate once — it's memory efficient and faster to start. Generator expressions are ideal for passing to functions like sum(), max(), or any(). Example: sum(x2 for x in range(107)) uses minimal memory, while sum([x2 for x in range(107)]) would create a huge list first.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
Are Python list comprehensions faster than for loops?
02
Can I use multiple if conditions in a list comprehension?
03
What's the difference between a list comprehension and a lambda with map()?
04
Can I use list comprehensions with dictionaries or sets?
🔥

That's Data Structures. Mark it forged?

6 min read · try the examples if you haven't

Previous
Sets in Python
5 / 12 · Data Structures
Next
Dictionary Comprehensions in Python