Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Python  Module 10 – Iterators, generators and functional tools

Generators and generator expressions

Write generator functions with yield, use generator expressions instead of lists, and measure the memory a generator saves on a million items.

  • Intermediate
  • 25 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free core, Pro depth: 9 of 25 minutes free
Free: A generator function runs its body in steps, pausing at every yield · A generator expression is a generator written in one line · A generator holds one value at a time: a million squares, measured · 3 more sections. With a pass: 2 in-depth parts and 3 interview answers, about 16 minutes.

What you will learn

  • Write generator functions with yield and predict when their body runs
  • Use generator expressions to feed sum(), any() and loops without building lists
  • Measure the memory a list and a generator need for the same work
  • Recover a generator's return value and recognise the bugs generators cause
  • Choose between a list and a generator from how the values are used

Before you start

What you’ll learn · Before you start

What you will learn

  • Write generator functions with yield and predict when their body runs
  • Use generator expressions to feed sum(), any() and loops without building lists
  • Measure the memory a list and a generator need for the same work
  • Recover a generator's return value and recognise the bugs generators cause
  • Choose between a list and a generator from how the values are used

Before you start

On this page

Adding up a million squares through a list holds every square at once, about 40 MB at the peak; through a generator expression it holds one at a time, and the peak rounds to 0 KiB. A generator function: A function whose body contains yield. Calling it runs none of the body and returns a generator; the body then runs a step at a time, up to the next yield, each time the generator is asked for an item. is a function whose body contains yield. Calling it runs none of the body: it returns a generator: The iterator a generator function returns. Each next() resumes the body where it paused, with its local variables kept, and when the body ends the generator raises StopIteration. Like every iterator, it is used up after one pass., an iterator that runs the body only when something asks for the next value.

A generator expression: A comprehension in round brackets, such as (x * x for x in xs). It builds no list: it makes each item only when asked for the next one, and it can be read through only once. More in “List, dict and set comprehensions” gives the same laziness in one line: sum(x * x for x in xs) adds the squares one at a time without building a list of them. Generators matter whenever data is large, slow to produce or endless, such as the lines of a big log file.

A generator function runs its body in steps, pausing at every yield

Each next() runs the body up to the following yield, hands that value out and pauses, keeping every local variable, until the next request resumes it from the same spot. The prints inside the body show when it runs:

The body of a generator runs in steps Python · pause_resume.py
def countdown(n):
    print("  body starts")
    while n > 0:
        print(f"  yield {n}")
        yield n
        n -= 1
    print("  body ends")


gen = countdown(2)
print("created a", type(gen).__name__, "- nothing has run yet")
print("got", next(gen))
print("got", next(gen))
try:
    next(gen)
except StopIteration:
    print("StopIteration")

Output

created a generator - nothing has run yet
  body starts
  yield 2
got 2
  yield 1
got 1
  body ends
StopIteration

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 pause_resume.py

The caller and the generator body take turns: each next() runs the body to the next yield, and the end of the body raises StopIteration.Callercountdown(2)bodynext()yield 2, then pausenext()yield 1, then pausenext()body ends:StopIteration

A generator and its caller take turns

Text description of the diagram

The diagram has two columns, the caller on the left and the body of countdown(2) on the right, with time running downwards.

  • The caller calls next(). The body starts, runs until yield 2, hands 2 back to the caller and pauses with its local variables kept.
  • The caller calls next() again. The body continues from the pause, runs until yield 1, hands 1 back and pauses again.
  • The caller calls next() a third time. The body continues and reaches its end, which raises StopIteration in the caller.

Creating the generator printed nothing from the body. The first next() ran it from the top to yield 2, and the second next() resumed it right after that yield, with n still holding 2, until yield 1. The third request let the loop finish, the body reached its end, and Python raised StopIteration. Step through the run and watch n:

Interactive · message by message countdown(2) and its caller take turns Step through pause_resume.py. Each next() runs the body to its next yield, where it pauses with n kept.
countdown(2) and its caller take turns, step 7 of 7. Messages so far: Your code to countdown(2): next(gen); countdown(2) to Your code: yield 2; Your code to countdown(2): next(gen); countdown(2) to Your code: yield 1; Your code to countdown(2): next(gen); countdown(2) to Your code: StopIteration.Your codeprinted StopIterationcountdown(2)finishednext(gen)yield 2next(gen)yield 1next(gen)StopIteration

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. Created, not started. gen = countdown(2) creates a generator and runs none of the body, so nothing is printed from it yet.
  2. The first next(). next(gen) starts the body. It prints "body starts" and "yield 2", and reaches yield n with n = 2.
  3. 2 comes out. yield hands 2 to the caller and pauses the body on that line, with n still 2. The caller prints "got 2".
  4. The second next(). next(gen) resumes the body right after the yield. It runs n -= 1, goes round the while loop and prints "yield 1".
  5. 1 comes out. yield hands 1 to the caller and pauses the body again. The caller prints "got 1".
  6. The third next(). The body resumes, n becomes 0 and the while loop ends. The body prints "body ends" and reaches the end of the function.
  7. StopIteration. A body that ends raises StopIteration in the caller. This program catches it and prints "StopIteration"; a for loop would simply stop.

Generators keep the iteration protocol of the previous lesson: a generator is its own iterator and is used up after one pass, like map() or a file.

Checkpoint

Check: Pausing at every yield

  1. Question 1 of 2 In pause_resume.py, what does the line gen = countdown(2) print?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Nothing at all

    A function whose body contains yield returns a generator without running any of its body. The body's first print() runs at the first next(gen).

  2. Question 2 of 2 The first next(gen) of pause_resume.py has just returned 2. Where is the body, and what does n hold?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Paused at yield n, with n = 2

    Each next() runs the body to the next yield and pauses there. The paused body keeps n = 2 until the second next() resumes it with n -= 1.

A generator expression is a generator written in one line

A generator function suits logic with several steps. A generator expression suits one filter or transformation, and both can be combined, as in this log of a payment service:

Only the error lines of a log, one at a time Python · error_lines.py
import io

LOG = io.StringIO(
    "10:00:01 INFO  payment 101 started\n"
    "10:00:02 ERROR payment 101 failed code=TIMEOUT\n"
    "10:00:02 INFO  payment 102 started\n"
    "10:00:03 INFO  payment 102 done\n"
    "10:00:05 ERROR payment 103 failed code=DECLINED\n"
    "10:00:07 ERROR payment 104 failed code=TIMEOUT\n"
)


def error_lines(lines):
    """Yield only the lines that report an error, one at a time."""
    for line in lines:
        if " ERROR " in line:
            yield line.rstrip("\n")


for line in error_lines(LOG):
    print(line)

LOG.seek(0)   # back to the start of the "file"
codes = (line.rsplit("code=", 1)[1] for line in error_lines(LOG))
print(sorted(set(codes)))

Output

10:00:02 ERROR payment 101 failed code=TIMEOUT
10:00:05 ERROR payment 103 failed code=DECLINED
10:00:07 ERROR payment 104 failed code=TIMEOUT
['DECLINED', 'TIMEOUT']

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 error_lines.py

error_lines() reads the file object line by line and yields only the lines that report an error, so a log of any size passes through it, one line at a time. The generator expression on the last lines builds on it: it takes each error line from the generator and keeps the code at its end. Nothing ran until set() asked codes for its first item. Step through those two lines, one log line at a time:

Interactive · stage by stage set(codes) pulls the log through two generators Step through codes = (… for line in error_lines(LOG)) and sorted(set(codes)). Each step is one log line on its way, or the end.
set(codes) pulls the log through two generators, step 8 of 8. Showing LOG, error_lines(LOG) (finished), codes (finished), set(codes) ({TIMEOUT, DECLINED}), sorted(set(codes)) (['DECLINED', 'TIMEOUT']).LOGsix lines of the logerror_lines(LOG)finishedcodesfinishedset(codes){TIMEOUT, DECLINED}sorted(set(codes))['DECLINED', 'TIMEOUT']two codes

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. Built, not run. The line codes = (… for line in error_lines(LOG)) calls error_lines(LOG), which only creates a generator, and creates the generator expression around it. No log line has been read.
  2. set() asks for an item. set(codes) asks codes for its first item, codes asks error_lines(), and error_lines() reads line 1. It is an INFO line, so its loop goes on to the next line.
  3. The first error line. Line 2 reports an error, so error_lines() yields it and pauses. The generator expression takes the text after code=.
  4. TIMEOUT reaches the set. codes hands TIMEOUT to set(), which keeps it and asks codes for the next item.
  5. Two INFO lines. error_lines() resumes, reads lines 3 and 4 and passes neither on, because both are INFO lines.
  6. DECLINED. Line 5 is an error line, and its code, DECLINED, travels through codes into the set.
  7. TIMEOUT again. Line 6 gives TIMEOUT once more. A set keeps one of each value, so it still holds two codes.
  8. The end. The log has no line 7, so error_lines() ends, codes ends, and set() is complete. sorted() then puts the two codes in order.

Generator expressions are written in round brackets, and the brackets can be dropped when the expression is the only argument of a call.

Checkpoint

Check: Generator expressions

  1. Question 1 of 2 Which call passes a generator expression to sum()?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: sum(x * x for x in xs)

    Round brackets around an expression with a for clause make a generator expression; when it is the only argument of a call, the call's brackets are enough.

  2. Question 2 of 2 In error_lines.py, how many lines of the log has error_lines() read when set() receives TIMEOUT for the first time?

    Choose one answer.

    A hint

    Follow the first item that set() asks for.

    Show the answer: Question 2 of 2

    Answer: Two, line 1 and line 2

    set() asks codes for an item, codes asks error_lines(), and error_lines() reads lines until one reports an error: line 1 is skipped and line 2 is yielded, so TIMEOUT arrives after two lines.

A generator holds one value at a time: a million squares, measured

The standard library’s tracemalloc traces the memory blocks Python allocates and records the peak. Adding a million squares through a list and through a generator expression gives the same total:

Peak memory of a list and of a generator Python · memory_peak.py
import tracemalloc


def peak_kib(compute):
    """Run compute() and return its result and the peak memory it allocated, in KiB."""
    tracemalloc.start()
    result = compute()
    peak = tracemalloc.get_traced_memory()[1]
    tracemalloc.stop()
    return result, peak / 1024


n = 1_000_000
a, list_peak = peak_kib(lambda: sum([x * x for x in range(n)]))
b, gen_peak = peak_kib(lambda: sum(x * x for x in range(n)))
print("same total:", a == b)
print(f"list comprehension:   {list_peak:9,.0f} KiB at the peak")
print(f"generator expression: {gen_peak:9,.0f} KiB at the peak")

Output

same total: True
list comprehension:      39,500 KiB at the peak
generator expression:         0 KiB at the peak

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 memory_peak.py

Version note

The recorded numbers come from 64-bit CPython, where each reference in a list takes 8 bytes and each of these integers 32 bytes. The browser runs a 32-bit build of Python with smaller objects, so it reports a smaller peak for the list; the generator stays close to zero on both.

In your browser, Pyodide 314.0.7 (CPython 3.14.2) prints:

same total: True
list comprehension:      23,527 KiB at the peak
generator expression:         0 KiB at the peak

The list comprehension held all million integers at once, about 39,500 KiB or 40 MB at the peak, before sum() read the first one. The generator expression produced each square, handed it to sum() and let it go, so its peak rounds to 0 KiB: it needs memory for one value at a time, however many there are. Generator expressions and generator functions share that property, because both produce one value per request. Numbers that must stay in memory fit in an array from the array module, with no object per value.

Checkpoint

Check: One value at a time in memory

  1. One question Why did the generator expression's peak round to 0 KiB in memory_peak.py?

    Choose one answer.

    Show the answer: One question

    Answer: It handed each square to sum() and let it go, so it held one value at a time

    The list comprehension held a million integers at once; the generator expression made one square per request, so its peak stays near zero however many squares there are.

An endless generator is safe while its consumer stops asking

A generator does not have to end. One that counts for ever is safe while its consumer stops asking:

Order numbers on demand Python · order_ids.py
from itertools import count, islice


def order_ids(prefix="ORD"):
    """An endless supply of order numbers."""
    for n in count(1):
        yield f"{prefix}-{n:05d}"


ids = order_ids()
print(list(islice(ids, 3)))       # the first three
print(next(ids))                  # carries on where islice stopped
print(list(islice(order_ids("RET"), 2)))

Output

['ORD-00001', 'ORD-00002', 'ORD-00003']
ORD-00004
['RET-00001', 'RET-00002']

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 order_ids.py

itertools.islice(ids, 3) takes three items and stops, and the next request carries on with number 4, because the paused generator kept its counter. list(order_ids()) without islice would never return. Step through the requests and watch the counter wait between them:

Interactive · message by message islice() takes three order numbers, and the generator waits Step through order_ids.py. The generator counts for ever, but it runs only while something asks it for an item.
islice() takes three order numbers, and the generator waits, step 8 of 8. Messages so far: islice(ids, 3) to ids: next(ids); ids to islice(ids, 3): ORD-00001; ids to islice(ids, 3): ORD-00002, ORD-00003; islice(ids, 3) to Your code: three order numbers; Your code to ids: next(ids); ids to Your code: ORD-00004.Your codeprinted ORD-00004islice(ids, 3)islice(…, 2) this timeidsa new one: RET, from 1next(ids)ORD-00001ORD-00002, ORD-00003three order numbersnext(ids)ORD-00004

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. Created. ids = order_ids() creates the generator. Its loop over count(1) has not started, so no number has been made.
  2. islice() asks. list(islice(ids, 3)) asks islice() for items, and islice() asks the generator for its first one.
  3. The first number. The generator takes 1 from count(1), yields ORD-00001 and pauses with n = 1.
  4. Two more. Two more requests give ORD-00002 and ORD-00003, and the generator pauses with n = 3.
  5. islice() stops asking. islice() has given its three items, so it asks for no more. The generator stays paused, and list() returns the three numbers.
  6. next(ids). next(ids) asks the same generator again. It resumes where it paused, so count(1) gives 4.
  7. ORD-00004. The generator yields ORD-00004 and pauses with n = 4. It has made four numbers, one for each request.
  8. A new generator. order_ids("RET") is a new generator with a count of its own, so its two numbers start again at 1, RET-00001 and RET-00002.

The last line calls order_ids("RET"), which makes a second generator with a counter of its own, so its numbers start again at 1 while ids stays paused at 4.

Collecting an endless generator

list(order_ids()), sorted(order_ids()) and max(order_ids()) each try to read every item, so none of them ever returns. Take a fixed number with islice(), or leave a loop with break.

Checkpoint

Check: Endless generators

  1. One question order_ids() is the generator function of order_ids.py. What does this print?

    Read the code, then choose one answer.

    ids = order_ids()
    print(list(islice(ids, 2)), next(ids))
    Show the answer: One question

    Answer: ['ORD-00001', 'ORD-00002'] ORD-00003

    An endless generator runs only while something asks for items: islice() takes two, and next() resumes the same paused counter for the third.

Three ways to write an iterator, and when each fits

The Countdown class of the previous lesson needed three methods; as a generator function it takes four lines, and as a generator expression one:

One countdown, written three ways Python · three_ways.py
class Countdown:
    """The iterator class of the previous lesson."""

    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current <= 0:
            raise StopIteration
        self.current -= 1
        return self.current + 1


def countdown(start):
    """The same countdown as a generator function."""
    while start > 0:
        yield start
        start -= 1


print(list(Countdown(3)))
print(list(countdown(3)))
print(list(n for n in range(3, 0, -1)))

Output

[3, 2, 1]
[3, 2, 1]
[3, 2, 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 three_ways.py

All three give [3, 2, 1]. The generator function keeps its place in a paused local variable, and the end of its body ends it: Python raises the StopIteration that the class had to raise by hand.

Iterator class, generator function or generator expression?
Criterion Iterator classGenerator functionGenerator expression
You write __iter__() and __next__()a def with yieldone expression in brackets
State is kept in attributes you updatelocal variables, pausedthe loop it describes
Extra methods or attributes any you neednone of your ownnone
Good for iterators with an interfacemulti-step logic, endless seriesone filter or transformation
When to choose You need methods beyond next()Most custom iterationFeeding sum(), any(), max() or a loop

Generators have a few more abilities. A return statement ends a generator, and its value travels inside the StopIteration exception. inspect.getgeneratorstate() tells whether a generator is created, running, suspended or closed. A StopIteration raised inside a generator’s body becomes a RuntimeError, since PEP 479, so it cannot end a loop silently.

Checkpoint

Check: Choosing the form of an iterator

  1. One question An iterator over the lines of a log must also offer a skipped() method that says how many blank lines it dropped. Which form fits?

    Choose one answer.

    Show the answer: One question

    Answer: An iterator class

    The table's rule: an iterator class when the object needs methods beyond next(), a generator function for most custom iteration, a generator expression to feed sum(), any(), max() or a loop.

Check yourself

Practice that adapts to you

Practise this lesson

11 questions about 12 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 11 A generator function load(path) starts with print("opening") and then yields the lines of the file. It is called as rows = load("marks.csv"). When is "opening" printed?

    Choose one answer.

    Show the answer: Question 1 of 11

    Answer: When something first asks rows for an item, for example with next(rows) or a for loop

    A function whose body contains yield returns a generator object without running any of its body. The body starts at the first next(), and runs only as far as the first yield.

  2. Question 2 of 11 What does pause_resume.py print?

    What does this program print? Choose one answer.

    def countdown(n):
        print("  body starts")
        while n > 0:
            print(f"  yield {n}")
            yield n
            n -= 1
        print("  body ends")
    
    
    gen = countdown(2)
    print("created a", type(gen).__name__, "- nothing has run yet")
    print("got", next(gen))
    print("got", next(gen))
    try:
        next(gen)
    except StopIteration:
        print("StopIteration")
    Show the answer: Question 2 of 11

    Answer: it prints

    created a generator - nothing has run yet
      body starts
      yield 2
    got 2
      yield 1
    got 1
      body ends
    StopIteration

    Calling countdown(2) only creates the generator: the body has not printed anything yet. Each next() runs the body up to the next yield and pauses there, so the prints of the body and of the caller take turns. The third next() lets the body run to its end, which raises StopIteration.

  3. Question 3 of 11 When can the round brackets of a generator expression be left out?

    Choose one answer.

    Show the answer: Question 3 of 11

    Answer: When it is the only argument of a call, as in sum(x * x for x in xs)

    Generator expressions are written in round brackets, and the brackets can be dropped when the expression is the only argument of a call.

  4. Question 4 of 11 What does this print?

    Read the code, then choose one answer.

    squares = (n * n for n in range(4))
    print(sum(squares), sum(squares))
    Show the answer: Question 4 of 11

    Answer: 14 0

    A generator expression is an iterator. The first sum() reads 0, 1, 4 and 9, so it returns 14 and leaves the generator used up; the second sum() gets no items and returns its start value, 0. There is no error.

  5. Question 5 of 11 Which line finds the length of the longest word in words without building a list, a set or any other collection of lengths?

    Choose one answer.

    Show the answer: Question 5 of 11

    Answer: max(len(w) for w in words)

    Feeding a generator expression straight to a function that reduces it, such as max(), sum() or any(), keeps one value at a time. Square brackets and braces build the whole collection first.

  6. Question 6 of 11 What did the list comprehension of memory_peak.py hold at its peak?

    Choose one answer.

    Show the answer: Question 6 of 11

    Answer: All million squares at once, before sum() read the first one

    The list peaked at about 39,500 KiB because it held all million integers at once; the generator expression peaked near 0 KiB because it held one.

  7. Question 7 of 11 The list comprehension of memory_peak.py peaked at about 39,500 KiB for 1,000,000 squares. About how many bytes is that per square?

    Type a number.

    A hint

    A KiB is 1,024 bytes.

    Show the answer: Question 7 of 11

    Answer: 40 bytes (anything from 38.5 to 41.5 counts)

    39,500 × 1,024 ÷ 1,000,000 ≈ 40.4 bytes per square: 8 bytes for the reference in the list and 32 for the integer object, the sizes the version note gives for 64-bit CPython.

  8. Question 8 of 11 Why does ids = order_ids() return at once, although the generator counts for ever?

    Choose one answer.

    Show the answer: Question 8 of 11

    Answer: Creating a generator runs none of its body

    A generator runs only on request, so an endless one is safe as long as its consumer stops asking, with islice() or a break.

  9. Question 9 of 11 order_ids() is the generator function of order_ids.py. What does this print?

    Read the code, then choose one answer.

    ids = order_ids("RET")
    next(ids)
    print(list(islice(ids, 2)))
    Show the answer: Question 9 of 11

    Answer: ['RET-00002', 'RET-00003']

    Every request resumes the same paused generator: next() takes the first number and islice() the next two.

  10. Question 10 of 11 How does a generator function say that it has no more items?

    Choose one answer.

    Show the answer: Question 10 of 11

    Answer: Its body ends, or runs a return statement, and Python raises StopIteration for the caller

    A generator function ends when its body ends or returns; Python then raises StopIteration on its behalf, which a for loop treats as the end.

  11. Question 11 of 11 The task is to add up the prices of the paid orders in orders. Which form fits best?

    Choose one answer.

    Show the answer: Question 11 of 11

    Answer: sum(o.price for o in orders if o.paid), a generator expression

    An iterator class for objects with methods, a generator function for multi-step or endless iteration, and a generator expression for one filter or transformation that feeds a function such as sum().

How a generator runs, with four bugs reproduced Pro: opens with a Pro, Premium or Ultimate pass

A generator is an object with a state, a paused frame and a way to hand back one final value. This part traces its states, recovers a return value in two ways and reproduces the four bugs that generators cause most often.

In this part:

  • Four states, traced from created to closed
  • Return values travel inside StopIteration
  • Four bugs reproduced: late checks, a stray StopIteration, late binding and an always-true object
  • Moving the checks to the call: the fix, run

2 diagrams · 5 runnable examples (Python) · about 12 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Memory and work, measured: when a generator wins and when a list is better Pro: opens with a Pro, Premium or Ultimate pass

A generator saves memory only when the values are used once, and it saves work only when the consumer can stop early. This part measures the memory at four sizes and counts the work, sets out when a list is the better choice and ends with an exercise.

In this part:

  • Peak memory at four sizes, list against generator
  • Work counted: early exit, and the cost of reading twice
  • Sizing a list before you build it, with numbers
  • Choosing between a list and a generator, with the trade-offs in a table
  • Exercise: chunks of any iterable, and primes that never end
    • Chunks of any iterable, and primes that never end

3 runnable examples (Python) · 1 exercise with sample tests · about 13 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher to mid · Warm-up

    What does calling a generator function return, and when does its body run?

    Calling a generator function, a function whose body contains yield, runs none of its body. It returns a generator object, which is an iterator. The body starts at the first next() call, or the first step of a for loop, and runs until it reaches a yield: the yielded value is handed to the caller and the function pauses with its local variables kept. Each later next() resumes it right after that yield. When the body finishes, the generator raises StopIteration, which ends a for loop quietly. Two consequences matter in practice: code at the top of the body, such as argument checks or opening a file, runs only when the first value is requested, and a generator, like every iterator, is used up after one pass.

  2. Mid to senior

    read_batches(path, size) is a generator function that starts by raising ValueError when size is below 1. A job calls it with size 0 at start-up, but the error appears twenty minutes later in a worker thread, and the traceback never shows the bad call. Explain what happened, and fix the function and its tests.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    A nightly report reads 50 million amounts from a database and prints their total, their average and the 10 largest. To save memory, a colleague replaced every list with a generator. Now the average and the top 10 are wrong, with no error. Explain why, estimate the memory involved, and design a version that is correct and memory-light.

    Model answer, Pro. First step and follow-ups

  4. Mid to senior

    A generator that reads records with line = next(it) inside a while True loop worked for years. After a Python upgrade, every run now ends with RuntimeError: generator raised StopIteration. What changed, why was the change made, and how do you fix the code?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A function with yield returns a generator, whose body runs only when values are requested and pauses at each yield with its local variables kept.
  • A generator expression, (expr for x in xs), is the one-line form; drop its brackets when it is a call’s only argument.
  • A generator holds one value at a time: a million squares peaked at about 40 MB through a list and under 1 KiB through a generator.
  • Generators are iterators, used up after one pass and cut with itertools.islice when they never end.
  • Choose a generator function for multi-step or endless iteration, a generator expression for one transformation, and an iterator class when the object needs methods of its own.

Terms in this lesson

generator
The iterator a generator function returns. Each next() resumes the body where it paused, with its local variables kept, and when the body ends the generator raises StopIteration. Like every iterator, it is used up after one pass.
generator function
A function whose body contains yield. Calling it runs none of the body and returns a generator; the body then runs a step at a time, up to the next yield, each time the generator is asked for an item.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.