Generator Expressions
A generator expression is a compact way to create a generator without writing a full def function with a yield statement. It looks almost exactly like a list comprehension, except it uses parentheses instead of square brackets — and that single difference in punctuation changes its behavior completely, from eager to lazy.
List Comprehension vs Generator Expression
Compare the two side by side. Both describe "the square of each number from 0 to 9," but they produce very different objects.
squares_list = [x**2 for x in range(10)] print(squares_list) print(type(squares_list)) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] # <class 'list'>
squares_gen = (x**2 for x in range(10))
print(squares_gen)
print(type(squares_gen))
# <generator object <genexpr> at 0x...>
# <class 'generator'>
for value in squares_gen:
print(value)
# 0
# 1
# 4
# ...
# 81The list comprehension builds the entire list of ten squared values immediately and stores all of them in memory before the line even finishes executing. The generator expression instead builds a small generator object that knows how to produce each square one at a time — nothing is actually computed until you iterate over it.
Memory And Behavior Comparison
Aspect | List Comprehension | Generator Expression |
|---|---|---|
Memory usage | All items are computed and stored in memory at once | Only one item at a time is held in memory as it is produced |
Evaluation timing | Eager — the whole list is built the moment the line runs | Lazy — values are computed on demand, as they are requested |
Reusability | Can be iterated over any number of times | Single-use — once fully iterated it is exhausted and yields nothing on a second pass |
Typical use case | Small-to-medium data you need to index, re-iterate, or measure with | Large or infinite sequences you only need to scan once, or feed straight into another function |
Passing A Generator Expression Directly Into A Function
When a generator expression is the only argument being passed to a function call, you can drop the extra pair of parentheses — the ones that belong to the function call double up as the generator expression's own parentheses.
total = sum(x**2 for x in range(1_000_000)) print(total)
data = [5, 12, 8, 150, 3] has_large_value = any(x > 100 for x in data) print(has_large_value) # True
In both examples, sum() and any() pull values from the generator expression one at a time and stop as soon as they have their answer — any() in particular can short-circuit and stop iterating the moment it finds a value greater than 100, without ever computing the rest. Writing sum((x**2 for x in range(1_000_000))) with the extra parentheses would also work, but it is unnecessary noise when the generator expression is the sole argument.
Choosing Between A List And A Generator
Reach for a list (or list comprehension) when you need to index into the results, iterate over them more than once, check its length with
len(), or pass it somewhere that specifically expects a list.Reach for a generator expression when you only need to iterate once — especially over a large dataset, a file, or a conceptually infinite sequence — and building the full collection in memory would be wasteful or impossible.
gen = (x for x in range(3)) print(list(gen)) # [0, 1, 2] - first pass consumes the generator print(list(gen)) # [] - second pass: already exhausted, nothing left