Warm-up · Activity 1 of 7
// I3.2 · ~32 min · Intermediate
Generators
After this lesson you can write generator functions with yield, predict when their code runs and what they produce, and hand work to another iterable with yield from.
Predict · Activity 2 of 7
Predict before you read on: in which order are the words printed?
def gen(): print("start", end=" ") yield 1 print("middle", end=" ") yield 2 g = gen() print("created", end=" ") print(next(g))Practice · Activity 3 of 7
Fill in the keyword that hands out each even number and keeps the loop going.
def evens(limit): n = 0 while n < limit: ____ n n += 2nPractice · Activity 4 of 7
Each call of counter() makes a new generator. What does this print?
def counter(): count = 0 while True: count += 1 yield count c = counter() next(c) next(c) print(next(c), next(counter()))Practice · Activity 5 of 7
What does this print?
def inner(): yield 1 yield 2 def outer(): yield 0 yield from inner() yield 3 print(list(outer()))Brain teaser · Activity 6 of 7
Brain teaser. A generator with a return in it. What does this print?
def first_two(items): for item in items: yield item if item == "b": return "done" print(list(first_two("abcd")))Apply · Activity 7 of 7
Mini-task. Write a generator fib(limit) that yields the Fibonacci numbers below limit: 0, 1, 1, 2, 3, 5, … where each number is the sum of the two before it. Keep the last two numbers in local variables. Print list(fib(50)) and list(fib(1)).
Check your work against this list
Build it yourself
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
Numbering a nested to-do list
Two small generators work together. flatten walks a nested list and uses yield from on itself for every inner list. numbered takes any iterable and yields "n. item" lines, keeping its counter n between calls. Neither builds a list: each line is made when the loop asks for it. A generator is an iterator, annotated as Iterator[...] from collections.abc.
main.py
from collections.abc import Iterable, Iterator
def flatten(items: list[object]) -> Iterator[object]:
"""Yield every item of a nested list, depth first."""
for item in items:
if isinstance(item, list):
yield from flatten(item)
else:
yield item
def numbered(lines: Iterable[object], start: int = 1) -> Iterator[str]:
"""Yield "n. line" for each line, counting from start."""
n = start
for line in lines:
yield f"{n}. {line}"
n += 1
todo: list[object] = ["shop", ["milk", "eggs", ["free-range"]], "call mum"]
steps = numbered(flatten(todo))
print(type(steps).__name__)
print(next(steps))
for line in steps:
print(line)
Run it with
python main.pyOutput
generator
1. shop
2. milk
3. eggs
4. free-range
5. call mum- numbered(flatten(todo)) ran no code yet: it only made a generator, as the first line shows.
- next(steps) took one line; the for loop carried on from the same generator at 2.
- free-range sits two lists deep, and yield from passed it up through both levels.
- n kept counting between lines because the generator keeps its local variables while it is paused.
Change it and run it
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Exercises
Exercise 1 of 3
Running totals, one at a time
running_total(numbers) in the starter builds a whole list of running totals: for [1, 2, 3, 4] it gives 1, 3, 6 and 10. Turn it into a generator that yields each total as soon as it is known and builds no list. The tests check the values and that running_total returns a generator.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Hints
Hint 1
Replace totals.append(total) with yield total, and delete the list.
Hint 2
A generator has no return value to annotate as a list: its return type is Iterator[int], imported from collections.abc.
Hint 3
total = 0; for n in numbers: total += n; yield total.
Show a solution
One way to solve it. Yours can look different and still pass the checks.
from collections.abc import Iterable, Iterator
def running_total(numbers: Iterable[int]) -> Iterator[int]:
"""Yield the running totals of numbers, one at a time."""
total = 0
for n in numbers:
total += n
yield total
if __name__ == "__main__":
print(list(running_total([1, 2, 3, 4])))
Run it on your computer
Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.py
from collections.abc import Iterable
def running_total(numbers: Iterable[int]) -> list[int]:
"""Return the running totals of numbers."""
totals = []
total = 0
for n in numbers:
total += n
totals.append(total)
return totals
if __name__ == "__main__":
print(list(running_total([1, 2, 3, 4])))
test_main.py
from types import GeneratorType
from main import running_total
def test_totals():
"""[1, 2, 3, 4] gives 1, 3, 6, 10"""
got = list(running_total([1, 2, 3, 4]))
assert got == [1, 3, 6, 10], f"list(running_total([1, 2, 3, 4])) gave {got!r}, expected [1, 3, 6, 10]"
def test_empty():
"""No numbers give no totals"""
got = list(running_total([]))
assert got == [], f"list(running_total([])) gave {got!r}, expected []"
def test_is_generator():
"""running_total returns a generator"""
got = running_total([1, 2])
assert isinstance(got, GeneratorType), f"running_total returned a {type(got).__name__}, not a generator: use yield"
def test_one_at_a_time():
"""next() gives the totals one by one"""
g = running_total(iter([5, 5, 5]))
got = next(g), next(g)
assert got == (5, 10), f"two next() calls gave {got!r}, expected (5, 10)"
On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.
Run the program:
python main.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyExercise 2 of 3
Flatten with yield from
Write a generator flatten(items) that yields every item of a nested list in order, however deep: flatten([1, [2, [3, 4]], 5]) gives 1, 2, 3, 4, 5. For an item that is a list, use yield from on flatten itself; yield any other item as it is. A string counts as a single item.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Hints
Hint 1
isinstance(item, list) tells you whether an item is itself a list.
Hint 2
For a list, the generator for that inner list is flatten(item). Pass on all its values with yield from.
Hint 3
if isinstance(item, list): yield from flatten(item) else: yield item.
Show a solution
One way to solve it. Yours can look different and still pass the checks.
from collections.abc import Iterator
def flatten(items: list[object]) -> Iterator[object]:
"""Yield every item of a nested list, depth first."""
for item in items:
if isinstance(item, list):
yield from flatten(item)
else:
yield item
if __name__ == "__main__":
print(list(flatten([1, [2, [3, 4]], 5])))
Run it on your computer
Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.py
from collections.abc import Iterator
def flatten(items: list[object]) -> Iterator[object]:
"""Yield every item of a nested list, depth first."""
for item in items:
yield item
if __name__ == "__main__":
print(list(flatten([1, [2, [3, 4]], 5])))
test_main.py
from main import flatten
def test_flat_list():
"""A flat list comes out unchanged"""
got = list(flatten([1, 2, 3]))
assert got == [1, 2, 3], f"list(flatten([1, 2, 3])) gave {got!r}"
def test_nested():
"""[1, [2, [3, 4]], 5] gives 1, 2, 3, 4, 5"""
got = list(flatten([1, [2, [3, 4]], 5]))
assert got == [1, 2, 3, 4, 5], f"list(flatten([1, [2, [3, 4]], 5])) gave {got!r}, expected [1, 2, 3, 4, 5]"
def test_empty_lists():
"""Empty inner lists add nothing"""
got = list(flatten([[], [1, []], []]))
assert got == [1], f"list(flatten([[], [1, []], []])) gave {got!r}, expected [1]"
def test_strings_stay_whole():
"""A string is one item, not split into letters"""
got = list(flatten(["ab", ["cd"]]))
assert got == ["ab", "cd"], f"list(flatten(['ab', ['cd']])) gave {got!r}, expected ['ab', 'cd']"
On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.
Run the program:
python main.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyExercise 3 of 3
An endless id generator
Write a generator ids(prefix) that yields prefix1, prefix2, prefix3 and so on without end: ids("u") gives "u1", "u2", "u3", …. Keep the counter in a local variable inside a while True loop. Two generators must count independently.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Hints
Hint 1
The starter yields once and then ends. Put the yield in a loop that never stops: while True:.
Hint 2
After the yield, move the counter on with n += 1; the generator pauses at yield, so this runs only when the next id is asked for.
Hint 3
Each call of ids() has its own n, so two generators never mix up their counts.
Show a solution
One way to solve it. Yours can look different and still pass the checks.
from collections.abc import Iterator
def ids(prefix: str) -> Iterator[str]:
"""Yield prefix1, prefix2, prefix3, ... without end."""
n = 1
while True:
yield f"{prefix}{n}"
n += 1
if __name__ == "__main__":
new_id = ids("u")
print(next(new_id), next(new_id))
Run it on your computer
Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.py
from collections.abc import Iterator
def ids(prefix: str) -> Iterator[str]:
"""Yield prefix1, prefix2, prefix3, ... without end."""
n = 1
yield f"{prefix}{n}"
if __name__ == "__main__":
new_id = ids("u")
print(next(new_id), next(new_id))
test_main.py
from main import ids
def test_first_three():
"""ids('u') gives u1, u2, u3"""
g = ids("u")
got = [next(g), next(g), next(g)]
assert got == ["u1", "u2", "u3"], f"three next() calls gave {got!r}, expected ['u1', 'u2', 'u3']"
def test_keeps_going():
"""The 100th id is still there"""
g = ids("x")
for _ in range(99):
next(g)
got = next(g, None)
assert got == "x100", f"the 100th id was {got!r}, expected 'x100'"
def test_independent():
"""Two generators count on their own"""
a, b = ids("a"), ids("b")
next(a)
next(a)
got = next(b), next(a)
assert got == ("b1", "a3"), f"got {got!r}, expected ('b1', 'a3')"
On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.
Run the program:
python main.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyCommon mistakes
Asking a generator for its length
def squares(n):
for i in range(n):
yield i * i
print(len(squares(5)))
What Python prints
TypeError: object of type 'generator' has no len()Why, and the fix
A generator makes its values on request, so it does not know how many there will be. If you need the count or the values twice, turn it into a list first: values = list(squares(5)), then len(values). If you only need the count, sum(1 for _ in gen) counts without keeping the values.
Indexing a generator
def squares(n):
for i in range(n):
yield i * i
print(squares(5)[0])
What Python prints
TypeError: 'generator' object is not subscriptableWhy, and the fix
A generator has no positions to look up, only a next value. Use next(squares(5)) for the first value, or list(squares(5))[0] when you really need random access.
yield outside a function
numbers = [1, 2, 3]
for n in numbers:
yield n * 2
What Python prints
SyntaxError: 'yield' outside functionWhy, and the fix
yield only makes sense inside a function, which it turns into a generator function. Wrap the loop in def doubled(numbers): and call it, or, for a quick result, write a list comprehension: [n * 2 for n in numbers].
Python in the browser: Pyodide 314.0.7, MPL-2.0. Licence and source
Exit ticket
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.