Python 3.12+ · Updated November 2026 · Every program explained line by line

Python Comprehensions
Build collections in one readable line

A complete, runnable reference for Python comprehensions. List, dict, set, and generator expressions — plus conditions, nesting, the walrus operator, and performance notes. Every code block includes a numbered line-by-line breakdown.

numbers = [1, 2, 3, 4, 5, 6]

squares = [n * n for n in numbers]
evens = [n for n in numbers if n % 2 == 0]
labels = ["even" if n % 2 == 0 else "odd" for n in numbers]

square_map = {n: n * n for n in numbers}
unique_mods = {n % 3 for n in numbers}
pair_sums = [(a, b, a + b) for a in numbers for b in numbers if a < b]

print(squares)
print(evens)
print(labels)
print(square_map)
print(unique_mods)
print(pair_sums)
Written & reviewed by Glenn Junsay Pansensoy Last reviewed: November 2026 Python 3.12+ ~25 min read

🎯 Why Comprehensions?

A comprehension is an expression that builds a new collection from an existing iterable

The classic way to build a list in a loop needs a manual accumulator, an explicit append call, and an extra variable that exists only to be filled. A comprehension rewrites all of that into a single expression that reads like a description of the result. Once you're used to them, the comprehension form is not just shorter — it's actually clearer, because the "input → transformation" relationship is visible at a glance.

📝 From for-loop to comprehension

numbers = [1, 2, 3, 4, 5]

# The explicit-loop version
squares_loop = []
for n in numbers:
    squares_loop.append(n * n)

# The comprehension version
squares_comp = [n * n for n in numbers]

print(squares_loop)
print(squares_comp)
print(squares_loop == squares_comp)
Line-by-line explanation
  1. 1A list of five numbers to work with.
  2. 3–5The explicit loop: create an empty list, iterate, square each value, append. Three lines, one of which is just a setup step.
  3. 8The comprehension: one expression that produces the same result. Read it as "n * n, for each n in numbers".
  4. 10–11Both print [1, 4, 9, 16, 25].
  5. 12Prints True — the two lists are equal. The comprehension is a drop-in replacement, not a different result.
Expected output[1, 4, 9, 16, 25] [1, 4, 9, 16, 25] True
💡 A reasonable rule of thumb: if the goal of a loop is "build a new list", reach for a comprehension. If the loop has side effects (printing, writing to a file, appending to multiple lists), keep the explicit loop.

The four comprehension types

List: [expr for x in it] — brackets.
Dict: {k: v for x in it} — braces with a colon.
Set: {expr for x in it} — braces, no colon.
Generator expression: (expr for x in it) — parentheses, lazy.

📋 List Comprehensions

The most-used form — wrapped in square brackets, builds a list eagerly

📋 Basic List Comprehension

Syntax: [expression for item in iterable]. The expression is evaluated once per item and the results are collected into a new list.

numbers = [1, 2, 3, 4, 5]

doubles = [n * 2 for n in numbers]
squares = [n ** 2 for n in numbers]
names = ["ana", "bob", "cara"]
capitalized = [name.capitalize() for name in names]
lengths = [len(name) for name in names]

print(doubles)
print(squares)
print(capitalized)
print(lengths)
Line-by-line explanation
  1. 1The input list — five integers.
  2. 3[n * 2 for n in numbers] — the expression n * 2 is applied to each element. Result: [2, 4, 6, 8, 10].
  3. 4[n ** 2 ...] — squaring. Result: [1, 4, 9, 16, 25].
  4. 5–6A list of strings. The comprehension calls .capitalize() on each name. Result: ['Ana', 'Bob', 'Cara'].
  5. 7Calls len() on each string. Result: [3, 3, 4].
  6. 9–12Print all four results — each is an independent new list.
💡 The expression can be anything — an arithmetic operation, a method call, a conditional, a function call, even another comprehension. The one thing it cannot do is contain a statement (assignments, print, etc.).

🔁 List Comprehension over Other Iterables

Any iterable works — strings, tuples, dicts, sets, range(), files, generators. A comprehension doesn't care what's being iterated.

word = "hello"
scores = {"Ana": 95, "Bob": 88}
values = {1, 2, 3}

chars = [c for c in word]
upper = [c.upper() for c in word]
keys = [k for k in scores]
tripled = [v * 3 for v in values]
range_sq = [n ** 2 for n in range(5)]

print(chars)
print(upper)
print(keys)
print(tripled)
print(range_sq)
Line-by-line explanation
  1. 1–3Four different iterables: a string, a dict, and a set.
  2. 5–6Iterating a string yields characters one at a time. chars is ['h', 'e', 'l', 'l', 'o']; upper is the uppercased version.
  3. 7Iterating a dict yields its keys, so keys is ['Ana', 'Bob'].
  4. 8Iterating a set yields its elements (in undefined order). tripled might be [3, 6, 9] or [6, 9, 3].
  5. 9range(5) yields 0–4. Result: [0, 1, 4, 9, 16].
💡 Because a comprehension works with any iterable, the same technique works on generators, file objects, and database cursors — which means you can process very large inputs without loading them into memory first.

📖 Dict Comprehensions

Build dictionaries from any iterable in one expression — {key: value for ...}

📖 Basic Dict Comprehension

Syntax: {key_expr: value_expr for item in iterable}. Both key and value are expressions evaluated per item.

numbers = [1, 2, 3, 4, 5]
names = ["ana", "bob", "cara"]

square_map = {n: n * n for n in numbers}
lengths = {name: len(name) for name in names}
by_first = {name[0]: name for name in names}

print(square_map)
print(lengths)
print(by_first)
Line-by-line explanation
  1. 1–2Input lists: five numbers and three names.
  2. 4{n: n * n for n in numbers} — the loop variable n appears on both sides. Result: {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}.
  3. 5{name: len(name) ...} — key is the string, value is its length. Result: {'ana': 3, 'bob': 3, 'cara': 4}.
  4. 6{name[0]: name ...} — key is the first letter. Result: {'a': 'ana', 'b': 'bob', 'c': 'cara'}. If two names started with the same letter, the later one wins.
  5. 8–10All three prints show dicts.
💡 If two items in the source produce the same key, the last one wins silently — no error, no warning. Check for uniqueness first if that matters (e.g. Counter or explicit length verification).

🔄 Transforming and Filtering Dicts

Dict comprehensions shine when you need to invert, filter, or reshape an existing dict.

scores = {"Ana": 95, "Bob": 88, "Cara": 72}

inverted = {score: name for name, score in scores.items()}
passing = {name: score for name, score in scores.items() if score >= 80}
uppercased = {name.upper(): score for name, score in scores.items()}
scaled = {name: score * 1.1 for name, score in scores.items()}

print(inverted)
print(passing)
print(uppercased)
print(scaled)
Line-by-line explanation
  1. 1A dict with three entries.
  2. 3Invert the mapping: keys become values, values become keys. Result: {95: 'Ana', 88: 'Bob', 72: 'Cara'}.
  3. 4Filter by score — only Ana and Bob pass. Result: {'Ana': 95, 'Bob': 88}.
  4. 5Uppercase the keys. Result: {'ANA': 95, 'BOB': 88, 'CARA': 72}.
  5. 6Multiply each score by 1.1. Result: values are 104.50000000000001, 96.8, 79.2 (floating-point precision again).
💡 Inverting a dict is safe only if the values are unique. If two names had the same score, one would be silently lost. Check uniqueness first: len(set(scores.values())) == len(scores).

🔵 Set Comprehensions

Build sets — duplicates are removed automatically, order is not preserved

🔵 Basic Set Comprehension

Syntax: {expression for item in iterable}. Braces, but no colon — that's what distinguishes it from a dict comprehension.

numbers = [1, 2, 2, 3, 3, 3]
words = ["apple", "banana", "avocado", "apple"]

unique_numbers = {n for n in numbers}
unique_lengths = {len(w) for w in words}
unique_firsts = {w[0] for w in words}

print(unique_numbers)
print(unique_lengths)
print(unique_firsts)
Line-by-line explanation
  1. 1A list with duplicates.
  2. 2A list of words, with "apple" repeated.
  3. 4{n for n in numbers} — the duplicates are automatically removed. Result: {1, 2, 3}.
  4. 5{len(w) for w in words} — different words with the same length produce the same value, and sets deduplicate. Result: {5, 6, 7} (apple=5, banana=6, avocado=7).
  5. 6{w[0] for w in words} — first letters. Result: {'a', 'b'}.
💡 Set comprehensions are the fastest way to deduplicate while applying a transformation: {normalize(s) for s in raw_strings} gives you unique normalised forms in one pass. For unique values in a list with order preserved, use dict.fromkeys() instead.

🧮 Set Comprehensions — Real Use Case

Finding the set of tags used across a collection of documents — a common task in text processing.

articles = [
    {"title": "A", "tags": ["python", "web"]},
    {"title": "B", "tags": ["python", "ai"]},
    {"title": "C", "tags": ["web", "ai", "python"]},
]

all_tags = {tag for article in articles for tag in article["tags"]}

print(sorted(all_tags))
Line-by-line explanation
  1. 1–5A list of three article dicts, each with a tags list.
  2. 7Nested set comprehension: for each article, iterate over its tags. The set deduplicates automatically. Result: {'python', 'web', 'ai'} (in some order).
  3. 9sorted() converts the set to a sorted list for predictable display. Result: ['ai', 'python', 'web'].
Expected output['ai', 'python', 'web']

♾️ Generator Expressions

Same syntax as a list comprehension, but with parentheses — and lazy evaluation

♾️ List vs Generator — Memory and Laziness

A list comprehension builds the whole list in memory; a generator expression yields values on demand. This matters when the input is large or the result is only needed once.

numbers = range(1_000_000)

list_version = [n * n for n in numbers]
gen_version = (n * n for n in numbers)

print(type(list_version).__name__)
print(type(gen_version).__name__)
print(sum(gen_version))
print(next((n * n for n in range(5))))
Line-by-line explanation
  1. 1A range of one million numbers — cheap to construct because it's lazy.
  2. 3[...] — a list comprehension that builds one million squared values immediately. Memory: several MB.
  3. 4(...) — a generator expression. Nothing is computed yet. Memory: negligible.
  4. 6–7Types: list vs generator.
  5. 8sum(gen_version) — consumes the generator, computing each square on demand. Constant memory. Returns the correct sum without ever building the full list.
  6. 9next(...) pulls one value from a fresh generator. Result: 0 (the square of 0, the first value of range(5)).
💡 A generator can only be iterated once. Once consumed by sum(), it's exhausted — list(gen_version) afterwards returns []. If you need to iterate multiple times, use a list comprehension or materialise the generator.

🔗 Generator Expressions Inside Function Calls

When the generator expression is the only argument to a function, the parentheses are optional. This is why sum(x * x for x in data) looks like it has only one set of parens.

numbers = [1, 2, 3, 4, 5]

total = sum(n * n for n in numbers)
evens = any(n % 2 == 0 for n in numbers)
all_pos = all(n > 0 for n in numbers)
biggest = max(n * 2 for n in numbers)

print(total)
print(evens)
print(all_pos)
print(biggest)
Line-by-line explanation
  1. 1A list of five numbers.
  2. 3sum(n * n for n in numbers) — no extra parens around the generator; sum receives the generator expression directly. Result: 1 + 4 + 9 + 16 + 25 = 55.
  3. 4any(...) — returns True as soon as it finds a truthy value, short-circuiting the rest. 2 is even, so it returns quickly.
  4. 5all(...) — returns True only if every value passes. All five are positive.
  5. 6max(...) — finds the largest doubled value: 10.
💡 Functions like sum, any, all, max, min are the ideal home for a generator expression — you compute each value once, in one pass, without building an intermediate list. This pattern is sometimes called the "feed-a-generator-to-a-reducer" style.

🔀 if/else Conditions

Filter (if after the for) vs transform (if before the for) — two very different behaviours

🔍 Filter with if After the for

An if placed after the for acts as a filter. Items that fail the condition are simply skipped.

numbers = [1, 2, 3, 4, 5, 6]

evens = [n for n in numbers if n % 2 == 0]
positives = [n for n in [-2, -1, 0, 1, 2] if n > 0]
with_b = [w for w in ["banana", "apple", "blueberry"] if "b" in w]

print(evens)
print(positives)
print(with_b)
Line-by-line explanation
  1. 1Numbers 1–6.
  2. 3if n % 2 == 0 filters for even values. Result: [2, 4, 6].
  3. 4if n > 0 filters out negatives and zero. Result: [1, 2].
  4. 5if "b" in w keeps only words containing the letter b. Result: ['banana', 'blueberry'].
💡 The if is evaluated once per item, and items that fail are dropped from the result entirely. The result list can be shorter than the input — even empty.

🎨 Ternary if/else Before the for

A ternary expression placed before the for transforms every item — nothing is filtered out. The output length always equals the input length.

numbers = [1, 2, 3, 4, 5]

labels = ["even" if n % 2 == 0 else "odd" for n in numbers]
clamped = [n if n <= 3 else 3 for n in numbers]

print(labels)
print(clamped)

# Both at once: filter AND transform
even_labels = ["even" if n < 4 else "big even" for n in numbers if n % 2 == 0]
print(even_labels)
Line-by-line explanation
  1. 1Numbers 1–5.
  2. 3A ternary if/else decides each value. Result: ['odd', 'even', 'odd', 'even', 'odd'] — same length as input.
  3. 4Clamp values at 3. Result: [1, 2, 3, 3, 3].
  4. 7Combined form: the if after for filters to evens, then the ternary before for chooses "even" or "big even" based on the value.
  5. 8Only evens pass the filter; among them, 2 is labelled "even" and 4 is labelled "big even". Result: ['even', 'big even'].
💡 The rule to remember: if after for filters; if/else before for transforms. If you write if before for without an else, Python raises a SyntaxError.

🧩 Nested Comprehensions

Multiple for clauses in one comprehension — powerful, but easy to over-use

🔁 Cartesian Product — Two Loops

Two for clauses produce every combination. The order matters: read left-to-right like the nested loops they replace.

letters = ["a", "b"]
numbers = [1, 2]

pairs = [(l, n) for l in letters for n in numbers]

print(pairs)

# Equivalent explicit loops
result = []
for l in letters:
    for n in numbers:
        result.append((l, n))

print(result)
print(pairs == result)
Line-by-line explanation
  1. 1–2Two short lists.
  2. 4The comprehension iterates l first (outer), then n (inner) — same order as the nested loops below. Result: [('a', 1), ('a', 2), ('b', 1), ('b', 2)].
  3. 7–10The explicit loops produce the same output. Read the comprehension left-to-right; the outermost for comes first.
  4. 12–13True confirms the comprehension is a drop-in replacement.
💡 Two-loop comprehensions are fine when both loops are short. Three or more nested loops are almost always better as explicit code — the reader has to mentally un-nest them.

🎯 Nested with a Condition on the Inner Loop

A filter if after the inner for applies only to the inner iteration.

numbers = [1, 2, 3, 4]

pairs_less = [(a, b) for a in numbers for b in numbers if a < b]

print(pairs_less)

pairs_same_parity = [(a, b) for a in numbers for b in numbers if (a + b) % 2 == 0 and a <= b]

print(pairs_same_parity)
Line-by-line explanation
  1. 1Numbers 1–4.
  2. 3All ordered pairs (a, b) where a < b. Result: [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)].
  3. 7Pairs where the sum is even (same parity) and a ≤ b. Result includes (1, 1), (1, 3), (2, 2), (2, 4), (3, 3), (4, 4).
💡 Conditions after the inner for can reference the outer loop variable — a is visible to b's condition. This is what makes "all pairs where a < b" expressible in one line.

🗺️ Nested Comprehensions for 2D Data

When the input is already nested (a list of lists), you'll often want one comprehension per level: an outer one for rows, an inner one for cells.

grid = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
]

doubled = [[cell * 2 for cell in row] for row in grid]
flattened = [cell for row in grid for cell in row]

print(doubled)
print(flattened)
Line-by-line explanation
  1. 1–5A 3×3 grid as a list of lists.
  2. 7Two separate comprehensions nested — the outer iterates rows, the inner iterates cells within each row. Result: a new 3×3 grid with every value doubled.
  3. 8One comprehension with two for clauses — this flattens the grid to a single list of 9 numbers.
💡 These are genuinely different shapes: [[...] for row in grid] produces a list of lists (same shape as input); [... for row in grid for cell in row] produces a flat list. Pick the one matching the output you actually want.

🦭 Walrus Operator in Comprehensions

Python 3.8+ — name an intermediate result once instead of computing it twice

🦭 Avoiding Double Computation

Without the walrus operator, a filter condition that uses an expensive value often forces you to compute that value twice — once for the condition, once in the expression. := lets you name it once.

def expensive(x):
    return x ** 2

data = range(10)

# Without walrus: expensive() is called twice per item
result_old = [expensive(x) for x in data if expensive(x) > 10]

# With walrus: expensive() is called once per item
result_new = [y for x in data if (y := expensive(x)) > 10]

print(result_old)
print(result_new)
Line-by-line explanation
  1. 1–2A simple function to simulate an expensive computation.
  2. 4Some data to iterate over.
  3. 7[expensive(x) for x in data if expensive(x) > 10] — expensive(x) runs twice for every item: once in the filter, once in the expression. Wasteful for genuinely expensive operations.
  4. 10[y for x in data if (y := expensive(x)) > 10] — expensive(x) runs once per item, and y is reused in the expression. Same result, half the work.
  5. 12–13Both lists are identical. The walrus version is semantically equivalent, just more efficient.
💡 Python 3.12 scopes the walrus variable y to the comprehension — after the comprehension ends, y is not defined in the enclosing scope. In Python 3.8–3.11, y leaked out; the change was a bug fix, not a feature.

⚡ Performance — Why Comprehensions Are Faster

Usually 20–40% faster than manual loops for simple transformations

⏱️ Comprehension vs for-loop

The main speedup comes from two things: comprehensions skip the repeated .append() method lookup, and they run their iteration in a specialised bytecode path.

import timeit

numbers = list(range(1000))

loop_time = timeit.timeit(
    "[].extend([]) or [n * n for n in numbers]",
    globals={},
    setup="numbers = list(range(1000))",
    number=1000,
)

comp_time = timeit.timeit(
    "[n * n for n in numbers]",
    setup="numbers = list(range(1000))",
    number=1000,
)

print(f"comprehension: {comp_time:.4f}s")
print(f"loop variant: {loop_time:.4f}s")
Line-by-line explanation
  1. 1Imports the standard-library timeit module.
  2. 3Test data — 1000 numbers.
  3. 5–10timeit.timeit(...) runs the statement 1000 times and returns the total seconds. The setup creates the data fresh each run.
  4. 12–17The same for the comprehension — typically 20–40% faster for this workload.
  5. 19–20Print the comparison.
💡 Microbenchmarks can be misleading. The honest answer: for 100-item lists, the difference is negligible. For million-item lists, the comprehension wins clearly. Use timeit on real data before optimising.

📊 Memory — the Bigger Difference

Speed is only half the story. The bigger gain from generator expressions is constant memory — useful when the input is large.

import sys

big_list = [n for n in range(100_000)]
big_gen = (n for n in range(100_000))

print(f"list: {sys.getsizeof(big_list):,} bytes")
print(f"generator: {sys.getsizeof(big_gen):,} bytes")
Line-by-line explanation
  1. 1Imports sys to call getsizeof.
  2. 3A list of 100,000 numbers — everything held in memory at once.
  3. 4A generator of 100,000 numbers — nothing computed yet.
  4. 6–7sys.getsizeof() returns the memory the object itself occupies. The list is roughly 800 KB; the generator is a few hundred bytes — regardless of how many values it will produce.
💡 When you only need to iterate once — for sum, any, max, or feeding a file — use a generator expression. When you need to iterate multiple times, index into the result, or pass it to something that expects a list, use a list comprehension.

🚫 Anti-patterns — When Not to Use a Comprehension

Comprehensions are elegant, but they have a boundary — past it, a loop is clearer

❌ Side Effects

A comprehension is meant to build a value, not to trigger effects. If the goal is to print, write, or mutate external state, use a loop.

# Don't do this — it builds a useless list of None
[print(n) for n in range(3)]

# Do this
for n in range(3):
    print(n)
Line-by-line explanation
  1. 2The comprehension runs print(3) three times and also builds [None, None, None] — a list you didn't want and probably won't use.
  2. 5–6The loop is clearly about triggering side effects. Same output, no wasted list.
💡 If the comprehension's result is discarded (nothing is assigned to a variable), you almost certainly want a loop. The sole exception is interactive REPL exploration, where typing [f(x) for x in data] just to see the result is fine.

❌ Deep Nesting

Three or more for clauses, or a mix of two if filters and two loops, becomes a puzzle to decode. Write the loop.

# Hard to read
result = [(a, b, c) for a in range(3) for b in range(3) for c in range(3) if a != b and b != c]

# Easier to read
result2 = []
for a in range(3):
    for b in range(3):
        for c in range(3):
            if a != b and b != c:
                result2.append((a, b, c))

print(result == result2)
Line-by-line explanation
  1. 2A three-deep comprehension with an and filter. It fits on one line, but reading it aloud takes real effort — and that's a sign.
  2. 5–10The nested-loop version: longer, but every piece is visible. Indentation makes the structure clear.
  3. 12Prints True — the two produce identical results.

❌ Comprehension Inside Comprehension When One Would Do

Stacking comprehensions where a single one plus a flat loop would work is a common source of confusion.

# Hard to read: two comprehensions, one inside the other's iterable
matrix = [[1, 2], [3, 4]]
result_hard = [[cell * 2 for cell in row] for row in [row + [row[0] * 2] for row in matrix]]

# Easier: two clear passes
augmented = [row + [row[0] * 2] for row in matrix]
result_easy = [[cell * 2 for cell in row] for row in augmented]

print(result_hard == result_easy)
Line-by-line explanation
  1. 3Two comprehensions inline — the inner one generates the iterable for the outer one, and both are in the same source line. Correct, but costs a re-read.
  2. 6–7The same logic in two named steps: augment each row, then double every cell. Longer, but each step has a name and a purpose.
  3. 9Prints True — both approaches are equivalent.
💡 The general principle: comprehensions should read like a description of a result. When they read like a puzzle, split into named intermediate variables — even if that means an extra line.

🏋️ Practice — Check Your Understanding

Three exercises with solutions. Try each before expanding the answer.

Exercise 1 — Predict the Output

What does each line print? Write your answers before expanding.

nums = [1, 2, 3, 4, 5]

print([n * 2 for n in nums if n > 2])
print(["big" if n > 3 else "small" for n in nums])
print({n: n * 2 for n in nums if n % 2 == 0})
print(len([n for n in nums for _ in range(2)]))
Show solution

Line 1: [6, 8, 10] — only 3, 4, 5 pass the filter; each is doubled.
Line 2: ['small', 'small', 'small', 'big', 'big'] — nothing is filtered; every value becomes a label.
Line 3: {2: 4, 4: 8} — only even numbers survive the filter.
Line 4: 10 — the comprehension's expression runs twice per element, so the output list has 10 items.

Exercise 2 — Rewrite as a Comprehension

Rewrite this loop as a list comprehension.

words = ["hello", "world", "python"]
result = []
for w in words:
    if len(w) > 5:
        result.append(w.upper())
Show solution
result = [w.upper() for w in words if len(w) > 5]

The filter goes after the for (only words longer than 5 characters pass). The transformation w.upper() goes before the for (every surviving word is uppercased). Result: ['PYTHON'] — "hello" and "world" are both 5 characters, so only "python" qualifies.

Exercise 3 — Word Length Map

Given a list of words, build a dict mapping each unique word length to the list of words of that length. Use a comprehension (possibly more than one).

words = ["hi", "hello", "be", "world", "python", "yo"]
Show solution
lengths = {len(w) for w in words}
grouped = {
    length: [w for w in words if len(w) == length]
    for length in lengths
}
print(grouped)

The outer dict comprehension iterates over the unique lengths; the inner list comprehension collects matching words. Result: {2: ['hi', 'be', 'yo'], 5: ['hello', 'world'], 6: ['python']}.

Note: this is a pattern where itertools.groupby or collections.defaultdict can be more efficient — the comprehension form re-scans the whole list once per unique length. For small inputs that's fine; for very large ones, prefer the specialised tools.

❓ Frequently Asked Questions

Common questions about comprehensions, answered directly

Use a comprehension when the goal is to build a new collection from an iterable in a single readable expression. Use an explicit for loop when the body has side effects (printing, appending to multiple lists, writing files), contains complex branching, needs break/continue, or is more than one line of logic. A comprehension should read like a description of the result, not like a mini-program.

Yes, usually 20–40% faster for simple cases. Comprehensions run their loop in a specialised bytecode path that avoids the per-iteration overhead of name lookups and method calls in an explicit for loop. The .append() call in a manual loop is the biggest cost — comprehensions do not pay it. For very small collections the difference is negligible; for large ones it matters.

Yes, but placement matters. A filter if goes after the for: [x for x in nums if x > 0]. A ternary expression goes before the for: [('pos' if x > 0 else 'neg') for x in nums]. You cannot use an if that filters values in the expression position — only the ternary form is allowed there.

A list comprehension [x for x in data] builds the entire list in memory eagerly. A generator expression (x for x in data) with parentheses produces values lazily on demand, using O(1) memory regardless of input size. Use a generator expression for large data or when you only need to iterate once (e.g. passing to sum, any, all, max).

Yes — since Python 3.8. The walrus operator := inside a comprehension lets you name an intermediate result that would otherwise be computed twice: [y for x in data if (y := expensive(x)) > 0]. The name y is scoped to the comprehension in Python 3.12+ — accessible only inside the brackets. In 3.8–3.11, it leaked out; the change was a bug fix.

Rarely. Nested comprehensions [x+y for x in a for y in b] are readable when short and the order of the fors matches the natural reading (outer loop first). Once you need three levels of nesting, or an if that references variables from more than one level, an explicit nested loop is almost always clearer. A good test: if you cannot read the comprehension out loud in one breath, it should be a for loop.

Educational Use Disclaimer

This page is part of Poetic Bytes, a free educational resource for learning Python programming. The code examples are provided for instruction and demonstration. They are not production-audited and should not be used as-is in security-sensitive, financial, or safety-critical systems without independent review. See our full disclaimer and terms of use.

🗣️ Learner Voices

No comments yet — your voice starts the conversation.