datarekha

Are list comprehensions faster than equivalent for-loops in Python, and when should you prefer a generator expression instead?

The short answer

List comprehensions are typically 20–50% faster than equivalent for-loops with list.append() because the bytecode is optimised and the attribute lookup for append is avoided. Generator expressions use O(1) memory versus O(n) for a comprehension, so prefer them when you only iterate once.

How to think about it

This is really two questions in one: do you understand why a comprehension is faster than a for loop with append, and do you know the memory trade-off that makes a generator expression the right tool sometimes? Both halves carry equal weight.

The speed comes from one specific cost. A plain loop that calls result.append(x) does a Python-level attribute lookup for append on every iteration. A list comprehension compiles to a dedicated LIST_APPEND bytecode that skips the lookup and runs in C — which is where the typical 20–50% speedup lives.

# Plain loop — an attribute lookup each pass
result = []
for x in range(100_000):
    result.append(x * x)

# List comprehension — same result, faster and more idiomatic
result = [x * x for x in range(100_000)]

Both build a fully materialised list — O(n) space. A generator expression is the same syntax with parentheses, but it produces values on demand, so it’s O(1) space:

total = sum(x * x for x in range(100_000))   # no list is ever built

A worked example

The three produce the same values; what differs is whether a list is built and how big it gets:

import sys

N = 6
loop = []
for x in range(N):
    loop.append(x * x)
comp = [x * x for x in range(N)]

print("for-loop  :", loop)
print("list comp :", comp)
print("genexp sum:", sum(x * x for x in range(N)))   # consumes without building a list

# Memory: the comprehension materialises 1000 ints; the genexp holds none
lst = [x * x for x in range(1000)]
gen = (x * x for x in range(1000))
print(f"list size  : {sys.getsizeof(lst)} bytes")
print(f"genexp size: {sys.getsizeof(gen)} bytes")
for-loop  : [0, 1, 4, 9, 16, 25]
list comp : [0, 1, 4, 9, 16, 25]
genexp sum: 55
list size  : 8856 bytes
genexp size: 112 bytes

The loop and the comprehension produce an identical list — the comprehension just gets there faster. The genexp computed the same sum (55) without ever materialising the list at all, which the sizes make vivid: a thousand stored squares cost 8,856 bytes, while the generator that can produce the same thousand costs 112 — because it holds a recipe, not results.

When to choose which

Reach for a list comprehension when you need the concrete list — random access (result[i]), len(), more than one pass, or to hand to something that requires a real sequence. Reach for a generator expression when you iterate exactly once (feeding sum(), any(), max()) or the dataset is big enough that materialising it would strain memory.

Learn it properly Comprehensions

Keep practising

All Python questions

Explore further

Skip to content