Generator Expressions
In short: (expr for item in iterable if cond) looks exactly like a
list comprehension, but with ( ) instead of [ ]. The difference is what
it hands back: a list comprehension builds the whole collection in memory,
right away; a generator expression hands out one value at a time, only
when asked, and never stores the rest. Feed it straight to sum(),
any(), all(), max() or next() and the list is never built at all. Drop
the parentheses when it's the only argument to a call: sum(x for x in nums)
reads cleaner than sum((x for x in nums)). Like map and filter, a
generator is used up after one full pass.
![A lazy tap dripping one numbered drop at a time into a hand, labelled "(x for x in nums)" — next to it, a full bucket already holding every drop at once, labelled "[x for x in nums]", with a note under the tap: "only what's asked for, ever exists at once"](/api/assets/python/assets/54623f5f358d4d02.png)
Total a huge order log
A store's order log has a million rows. Summing the prices doesn't need all of them sitting in memory at once — it only ever needs the running total and whichever price it's looking at right now:
Same answer, wildly different memory. sys.getsizeof on the generator
reports a small, fixed size — it's a tiny machine that knows how to
produce the next value, not a container holding a million of them.
Stop at the first order over budget
next(gen, default) pulls exactly one value and stops — perfect for "find
the first thing that matches" without scanning, building, or even looking
at, everything after it:
The generator remembers exactly where it paused. The second next() doesn't
rescan from the start — it picks up right after 120 and finds 200. A list
comprehension would have built [120, 200] in full before you ever asked for
the first item.
The reuse trap
A generator is a one-way street. Once it's been walked, it reports nothing
else — even a fresh for loop over it finds it empty:
Run it and notice the total is wrong, not an error. Then get both the highest value and the true total from the same data.
Fix it by keeping a list when you need more than one pass: readings = [12, 45, 8, 90], then max(readings) and sum(readings) both work, because
a list can be walked again and again. Only reach for a generator expression
when the data is walked exactly once.
Quick practice
nums = [1, 2, 3]
squares = (n * n for n in nums)
print(type(squares).__name__)nums = [-3, 5, -1, 8]
print(any(n < 0 for n in nums))g = (x for x in range(3))
first_pass = list(g)
second_pass = list(g)
print(first_pass, second_pass)prices = [100, 250, 80]
total = sum(p for p in prices if p > 90)
print(total)🎯 Your turn
Write first_over_budget(orders, budget). Build a generator expression that
keeps only the order amounts above budget, then use next(..., None) to
return the first one — without ever building a list of all of them.
Return None if nothing qualifies.
first_over_budget([50, 80, 120, 200], 100)→120first_over_budget([10, 20], 100)→None
Hint: over = (o for o in orders if o > budget), then
return next(over, None).
Press ▶ Run, try the Live App chips, then ✓ Check. After that, the practice set below goes from a single filtered sum to a small stats report built entirely out of generators.
