Exercises — Generator expressions — memory efficiency
Before we begin, one picture to keep in your head the whole way down.

The list on the left has all its boxes filled at once. The generator on the right holds exactly one box; the rest are just a promise (dotted). Every exercise below is really asking: "which side of this picture are you on right now?"
Level 1 — Recognition
Goal: can you tell a generator from a list at a glance, and read what it holds?
Recall Solution 1.1
Only b is a generator expression.
auses square brackets[]→ alist.buses round brackets()→ a generator (the one lazy object here).cuses curly brackets{}→ aset(eager, all built now).dcontains a generator expression, butlist(...)immediately drains it into alist. The final objectdis a list.
Rule: the punctuation around the comprehension decides the type. See List comprehensions for the eager [] cousin.
Recall Solution 1.2
<class 'generator'>.
A common wrong guess is tuple, because round brackets usually make tuples. But a tuple has no for inside — (1, 2, 3) is a tuple, while (n for n in range(5)) is a generator. The presence of for ... in ... inside the parentheses is what flips it.
Level 2 — Application
Goal: use a generator correctly with next(), sum(), for, and filters.
Recall Solution 2.1
0, then 10, then 20.
Each next(g) pulls one value, advances the internal position, and forgets the previous one — exactly the single-box picture at the top. A fourth next(g) would raise StopIteration, because there are only three values to give.
Recall Solution 2.2
9.
The filter if x % 2 == 1 keeps the odd numbers from 1..5: that is 1, 3, 5. Their sum is . sum never sees a list — it receives each odd number one at a time and adds it to a running total.
Recall Solution 2.3
biggest = max(len(w) for w in ["hi", "world", "ok"])When a generator expression is the only argument to a call, Python lets you drop the inner parentheses. Answer value: the word lengths are 2, 5, 2, so max returns 5.
Level 3 — Analysis
Goal: reason about memory cost and exhaustion — not just run code, but explain it.
Recall Solution 3.1
Same result, very different peak memory.
Afirst builds a list of one million integers, then sums it. Peak extra memory is — it holds all items simultaneously (~8 MB+ of pointers plus the ints).Bstreams one square at a time intosum; only one value exists at any instant, so peak extra memory is .
Both compute the same total, so the time is comparable; the space is the whole point. See Big-O space complexity.
Recall Solution 3.2
Line 1: [0, 2]. Line 2: [].
The first list(g) walks the generator all the way to the end, collecting the even numbers 0, 2. That single pass exhausts the generator. The second list(g) finds nothing left to yield, so it returns an empty list. A generator does not rewind. This is the exhaustion behaviour from the parent note — see also Iterators and the iterator protocol.
Recall Solution 3.3
Version 1 reads less.
- Version 1 uses a generator with
next. It stops the moment it finds the first"ERROR"line and reads no further. This is Lazy evaluation in action. - Version 2 builds a full list of every error line (scanning the entire file), then throws all but
[0]away. It does maximum work to keep one item.
Same answer, but Version 2 may read millions of lines Version 1 never touches.
Level 4 — Synthesis
Goal: combine generators, chain them, and choose the right tool.
Recall Solution 4.1
[0, 1, 4, 9, 16].
squares would yield 0, 1, 4, 9, 16, 25. The second generator small pulls from squares one value at a time and passes through only those < 20. 25 is dropped. Crucially, no full list of squares is ever built — values flow one-by-one through the whole pipeline. This is composition of lazy iterators.
Recall Solution 4.2
The generator is exhausted by the first list(evens). If you genuinely need the data twice, materialise it once into a list:
evens = [n for n in range(10) if n % 2 == 0] # a real list, reusable
print(evens) # [0, 2, 4, 6, 8]
print(sum(evens)) # 20Now evens is a list, so both passes see the full data. Sum: .
Rule of thumb: need it once and lazily → generator. Need it many times → list.
Recall Solution 4.3
lengths = (len(w) for w in words if len(w) > 2)
print(list(lengths)) # [3, 3, 8]Words longer than 2 letters: "cat" (3), "dog" (3), "elephant" (8). "ox" (2) is filtered out. Result: [3, 3, 8].
Level 5 — Mastery
Goal: the subtle edge cases — empty inputs, late binding, side effects, no subscripting.
Recall Solution 5.1
list(x for x in range(0))→[].range(0)yields nothing, so the generator is empty from birth.sum(x for x in [])→0. Summing zero numbers gives the additive identity0.next((x for x in []), "none")→"none". When a generator is empty,nextwith a default returns that default instead of raisingStopIteration.
Edge cases matter: an empty generator is legal and common — always safe, never an error unless you call next on it without a default.
Recall Solution 5.2
TypeError: 'generator' object is not subscriptable.
A generator has no stored elements to index into — there is no "slot 0" sitting in memory. To get the first item use next(g); to get an arbitrary position you must iterate up to it (or convert to a list first, paying the memory cost).
Recall Solution 5.3
[0, 100, 200].
This is the deep one. The generator does not compute anything at creation time — it is lazy. It only runs its body when list(g) iterates it, and by then m is 100. So it uses the current value of the free variable m, not the value at the moment the generator was written. Because iteration is deferred, the environment it reads is also deferred.
Contrast: a list comprehension [x * m for x in range(3)] would evaluate immediately with m == 2, giving [0, 2, 4].
Recall Solution 5.4
Output order:
built generator
made 0
first: 0
make runs zero times when the generator is built — creation does no work. It runs once on the first next(g), printing made 0, then yielding 0. The values made 1 / made 2 never appear because we never asked for them. This is the printer-prints-on-demand model made visible. See Lazy evaluation and Generator functions and yield.
Recall Feynman check — say it in one breath
A generator is a recipe that runs on demand: it holds no data, does no work, and reads its variables only when you pull a value. That is why it costs memory, why it exhausts after one pass, why it can't be indexed, why empty inputs are harmless, and why a variable changed after building it still affects the output. All five behaviours are the same fact — "compute later, one at a time" — seen from different angles.
Connections
- Generator expressions — memory efficiency — the parent concept these exercises drill
- List comprehensions — the eager
[]counterpart (Exercises 1.1, 4.2, 5.3) - Iterators and the iterator protocol — exhaustion and
nextcome from here (Ex 3.2, 5.2) - Generator functions and yield — the
def/yieldcousin (Ex 5.4) - Lazy evaluation — the principle behind early-exit and late binding (Ex 3.3, 5.3, 5.4)
- Big-O space complexity — vs (Ex 3.1)
- Memory management in Python — why the list allocation in Ex 3.1 is costly