Skip to content
aviral gupta

// I3.3 · ~26 min · Intermediate

Generator expressions and lazy evaluation

After this lesson you can feed generator expressions straight into sum, max, join, any and all, predict where any and all stop, and avoid reusing a used-up generator.

Lesson 3 of 6 in I3 Iteration and functional tools

You will be able to

  • Write generator expressions and pass them straight to sum, min, max and join
  • Use any() and all() with a generator expression, and predict where they stop
  • Predict what happens when a generator expression is used twice or evaluated lazily
  1. Warm-up · Activity 1 of 7

    Warm-up from module B4: a list comprehension with a condition. What does this print?

    print([n * 2 for n in range(4) if n % 2 == 0])
  2. Predict · Activity 2 of 7

    Predict before you read on: the same squares are summed twice. What does this print?

    squares = (n * n for n in range(4))
    print(sum(squares), sum(squares))
  3. Practice · Activity 3 of 7

    Fill in the keyword that completes the generator expression, so that the program prints the total number of letters.

    words = ["hi", "there"]
    print(sum(len(w) ____ w in words))
    sum(len(w) w in words)
  4. Practice · Activity 4 of 7

    What does this print?

    nums = [3, 8, 12]
    print(any(n > 10 for n in nums), all(n > 2 for n in nums))
  5. Practice · Activity 5 of 7

    Match each expression to its value.

  6. Brain teaser · Activity 6 of 7

    Brain teaser. check() prints each number it looks at. What does the program print?

    def check(n):
        print("checking", n, end="; ")
        return n > 1
    
    print(any(check(n) for n in [0, 2, 5]))
  7. Apply · Activity 7 of 7

    Mini-task. orders = [("pen", 1.5, 4), ("book", 12.0, 2), ("lamp", 35.0, 1)] holds name, price and quantity. With one generator expression each, print the total price as total: 65.00, whether every quantity is above 0, and whether any price is above 30. Build no lists.

    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

Statistics from a web server log

Each log line holds a method, a path, a status code and the bytes sent. Every statistic is one generator expression passed straight to sum, any, all, max or join, so no list of numbers is ever built. The program also sums the same generator twice, to show that the second pass finds nothing.

main.py

log = [
    "GET /index.html 200 512",
    "GET /missing 404 0",
    "POST /login 200 128",
    "GET /about.html 200 2048",
    "GET /admin 403 0",
]

sizes = (int(line.split()[3]) for line in log)
print("bytes sent:", sum(sizes))
print("bytes again:", sum(sizes))

print("any errors:", any(int(line.split()[2]) >= 400 for line in log))
print("all GET:", all(line.startswith("GET") for line in log))
print("largest:", max(int(line.split()[3]) for line in log))
empty = ", ".join(line.split()[1] for line in log if line.endswith(" 0"))
print("empty replies:", empty)

Run it with

python main.py

Output

bytes sent: 2688
bytes again: 0
any errors: True
all GET: False
largest: 2048
empty replies: /missing, /admin
  • sizes was used up by the first sum, so the second sum added nothing and gave 0.
  • any stopped at the second line, the first with a status of 400 or more.
  • all stopped at the POST line: one false item is enough for False.
  • The if clause works as in a list comprehension: join got only the paths of lines ending in " 0".
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 2

Word statistics in one line each

Finish three functions, each with one generator expression. total_length(words) returns the number of letters in all words together. has_long_word(words, limit) returns True if any word is longer than limit. all_lowercase(words) returns True if every word is all lowercase, which str.islower() tells you; it is True for an empty list.

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

    Each body is a single return with sum, any or all around a generator expression.

  2. Hint 2

    sum(len(w) for w in words) adds the lengths; any(len(w) > limit for w in words) checks for one long word.

  3. Hint 3

    all(w.islower() for w in words) is already True for an empty list, so no extra if is needed.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

def total_length(words: list[str]) -> int:
    """Return the number of letters in all words together."""
    return sum(len(w) for w in words)


def has_long_word(words: list[str], limit: int) -> bool:
    """Return True if any word is longer than limit."""
    return any(len(w) > limit for w in words)


def all_lowercase(words: list[str]) -> bool:
    """Return True if every word is all lowercase."""
    return all(w.islower() for w in words)


if __name__ == "__main__":
    words = ["tea", "Coffee", "water"]
    print(total_length(words), has_long_word(words, 5), all_lowercase(words))
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

def total_length(words: list[str]) -> int:
    """Return the number of letters in all words together."""
    return 0


def has_long_word(words: list[str], limit: int) -> bool:
    """Return True if any word is longer than limit."""
    return False


def all_lowercase(words: list[str]) -> bool:
    """Return True if every word is all lowercase."""
    return False


if __name__ == "__main__":
    words = ["tea", "Coffee", "water"]
    print(total_length(words), has_long_word(words, 5), all_lowercase(words))

test_main.py

from main import all_lowercase, has_long_word, total_length


def test_total_length():
    """tea, Coffee and water have 14 letters together"""
    got = total_length(["tea", "Coffee", "water"])
    assert got == 14, f"total_length(['tea', 'Coffee', 'water']) returned {got!r}, expected 14"


def test_has_long_word():
    """Coffee is longer than 5 letters, but no word is longer than 6"""
    words = ["tea", "Coffee", "water"]
    got = has_long_word(words, 5), has_long_word(words, 6)
    assert got == (True, False), f"has_long_word with limits 5 and 6 returned {got!r}, expected (True, False)"


def test_all_lowercase():
    """Coffee is not lowercase; tea and water are"""
    got = all_lowercase(["tea", "Coffee"]), all_lowercase(["tea", "water"])
    assert got == (False, True), f"all_lowercase returned {got!r}, expected (False, True)"


def test_empty_list():
    """For no words: 0 letters, no long word, and all lowercase"""
    got = total_length([]), has_long_word([], 0), all_lowercase([])
    assert got == (0, False, True), f"for [] the functions returned {got!r}, expected (0, False, True)"

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 2

A summary that survives a generator

summary(numbers) returns the smallest value, the largest value and the mean. The starter works for a list but fails when numbers is a generator, because min uses it up. Make it work for any iterable, with a single pass over numbers. For no numbers at all, it raises ValueError.

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

    A generator can be read only once, but a list can be read as often as you like.

  2. Hint 2

    Start with values = list(numbers). That is the single pass; after it, use values everywhere.

  3. Hint 3

    if not values: raise ValueError("no numbers"). Then return min(values), max(values), sum(values) / len(values).

Show a solution

One way to solve it. Yours can look different and still pass the checks.

from collections.abc import Iterable


def summary(numbers: Iterable[float]) -> tuple[float, float, float]:
    """Return the smallest, the largest and the mean of numbers."""
    values = list(numbers)
    if not values:
        raise ValueError("no numbers")
    return min(values), max(values), sum(values) / len(values)


if __name__ == "__main__":
    print(summary(n * 1.5 for n in [2, 4, 6]))
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 summary(numbers: Iterable[float]) -> tuple[float, float, float]:
    """Return the smallest, the largest and the mean of numbers."""
    smallest = min(numbers)
    largest = max(numbers)
    mean = sum(numbers) / sum(1 for _ in numbers)
    return smallest, largest, mean


if __name__ == "__main__":
    print(summary(n * 1.5 for n in [2, 4, 6]))

test_main.py

from main import summary


def test_list():
    """A list [3, 1, 2] gives (1, 3, 2.0)"""
    got = summary([3, 1, 2])
    assert got == (1, 3, 2.0), f"summary([3, 1, 2]) returned {got!r}, expected (1, 3, 2.0)"


def test_generator():
    """A generator expression works too"""
    try:
        got = summary(n * 1.5 for n in [2, 4, 6])
    except ValueError as error:
        raise AssertionError(f"summary(<generator>) raised ValueError: {error}. The generator was used up by an earlier pass") from None
    assert got == (3.0, 9.0, 6.0), f"summary(n * 1.5 for n in [2, 4, 6]) returned {got!r}, expected (3.0, 9.0, 6.0)"


def test_single_value():
    """One value is the smallest, the largest and the mean"""
    got = summary(iter([7]))
    assert got == (7, 7, 7.0), f"summary(iter([7])) returned {got!r}, expected (7, 7, 7.0)"


def test_empty():
    """No numbers raise ValueError"""
    try:
        got = summary([])
    except ValueError:
        return
    raise AssertionError(f"summary([]) returned {got!r} instead of raising ValueError")

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

Using a generator expression twice

prices = [4.5, 12.0, 3.25]
doubled = (p * 2 for p in prices)
total = sum(doubled)
biggest = max(doubled)
print(total, biggest)

What Python prints

ValueError: max() iterable argument is empty

Why, and the fix

sum used up the generator, so max got an empty iterator. When you need the values more than once, build a list with square brackets: doubled = [p * 2 for p in prices]. Or write a fresh generator expression for each use.

A generator expression next to another argument

nums = [1, 2, 3]
print(sum(n * n for n in nums, 10))

What Python prints

SyntaxError: Generator expression must be parenthesized

Why, and the fix

The brackets of a generator expression may only be left out when it is the only argument of the call. With a second argument it needs its own: sum((n * n for n in nums), 10). Here sum starts counting at 10 and gives 24.

Joining numbers

ids = [7, 12, 30]
print(", ".join(n for n in ids))

What Python prints

TypeError: sequence item 0: expected str instance, int found

Why, and the fix

join glues strings only, and the generator hands it ints. Convert inside the expression: ", ".join(str(n) for n in ids) prints 7, 12, 30.

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

A comprehension in round brackets

(x * x for x in range(10)) looks like a list comprehension, but in round brackets it gives a generator: an iterator that makes each value when asked. Nothing is computed when the expression is created. When it is the only argument of a call, its own brackets can go: sum(x * x for x in nums). Use one when the values are consumed once, right away, by sum, min, max, "".join, set or a for loop. It never holds all the values in memory, which suits large inputs.

any() and all() stop early

any(iterable) returns True as soon as one item is true; all(iterable) returns False as soon as one item is false. Given a generator expression, they stop consuming it right there: any(n > 100 for n in nums) does not look at the rest of nums after the first big number. For an empty iterable, any gives False and all gives True. So all(...) reads as "every item passes", and any(...) as "at least one does".

One pass, and later than you think

A generator expression is an iterator, so it is used up after one pass. total = sum(g) followed by max(g) fails: max gets an empty iterator and raises ValueError. Store the values in a list when you need them twice. Laziness also moves errors: in (10 / d for x in nums), a ZeroDivisionError appears only when a value is asked for, not where the expression is written. Only the iterable of the first for, here nums, is evaluated at once.

Sources

Last reviewed September 29, 2026