Skip to content
aviral gupta

// 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.

Lesson 2 of 6 in I3 Iteration and functional tools

You will be able to

  • Write a generator function with yield and predict the values it produces
  • Explain how a generator pauses at yield and keeps its local state between next() calls
  • Delegate to another iterable or generator with yield from
  1. Warm-up · Activity 1 of 7

    Warm-up from lesson I3.1: which two methods must an iterator have? Pick both.

    Select all that apply.

  2. 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))
  3. 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 += 2
    n
  4. Practice · 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()))
  5. 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()))
  6. 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")))
  7. 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.py

Output

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
  1. Hint 1

    Replace totals.append(total) with yield total, and delete the list.

  2. Hint 2

    A generator has no return value to annotate as a list: its return type is Iterator[int], imported from collections.abc.

  3. 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.py

Run the checks (needs learnrun.py in the same folder):

python learnrun.py test
Download learnrun.py

Exercise 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
  1. Hint 1

    isinstance(item, list) tells you whether an item is itself a list.

  2. Hint 2

    For a list, the generator for that inner list is flatten(item). Pass on all its values with yield from.

  3. 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.py

Run the checks (needs learnrun.py in the same folder):

python learnrun.py test
Download learnrun.py

Exercise 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
  1. Hint 1

    The starter yields once and then ends. Put the yield in a loop that never stops: while True:.

  2. 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.

  3. 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.py

Run the checks (needs learnrun.py in the same folder):

python learnrun.py test
Download learnrun.py

Common 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 subscriptable

Why, 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 function

Why, 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.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

yield makes a generator function

A function that contains yield is a generator function. Calling it does not run the body: it returns a generator object, which is an iterator. Each next() runs the body up to the next yield, hands out that value and pauses there. When the body ends, by running off its end or with a bare return, the generator raises StopIteration. So a for loop, list() or sum() consume it like any iterator, and you never write __iter__ or __next__: the generator has them already.

Paused, not finished

Between two next() calls a generator keeps everything: its local variables, and where it is in a loop. That makes it much shorter than an iterator class, where you store the position in self.index yourself. It also means work happens only when a value is asked for: code before the first yield runs at the first next(), not at the call. A while True loop is fine, because only the values someone asks for are made. Like any iterator, a generator is used up after one pass; call the function again for a fresh one.

yield from

yield from iterable hands out every value of another iterable, one at a time, as if you had written for x in iterable: yield x. The iterable can be a list, a string or another generator. It is handy for splitting a generator into parts, and for nested data: a generator that walks a nested list can yield from itself for each inner list. Do not confuse it with yield: yield [1, 2] hands out the list as one value, yield from [1, 2] hands out 1 and then 2.

Sources

Last reviewed September 29, 2026