Are list comprehensions faster than equivalent for-loops in Python, and when should you prefer a generator expression instead?
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.