Previous | Tutorial index | Next
yield and Lazy Evaluationyield keyword.Generators are a special kind of function that return values lazily—they produce one value at a time, pausing between yields, rather than computing all values at once and returning a collection. This makes them extremely memory‑efficient and ideal for working with large datasets, streaming data, or infinite sequences. In this tutorial, you’ll learn how to define generators using yield, how they maintain state, their advantages over regular functions, and how to use generator expressions for concise lazy code.
A generator is a function that contains one or more yield expressions. When called, it returns a generator iterator (often just called a generator) that can be iterated over. Each time you request the next value (e.g., with next() or in a for loop), the generator executes until it hits a yield, returns the yielded value, and suspends its state—local variables, instruction pointer, etc.—until the next request.
yield Keywordyield is similar to return, but instead of terminating the function, it pauses it and remembers where it left off. When the generator is resumed, it continues from that point.
def simple_generator():
yield 1
yield 2
yield 3
gen = simple_generator()
print(next(gen)) # 1
print(next(gen)) # 2
print(next(gen)) # 3
# next(gen) # StopIteration
When a generator function is called, its code is not executed immediately; instead, a generator object is returned. Each call to next() (or iteration) runs the code until the next yield. Local variables and the execution state are saved between calls.
def count_down(n):
print("Starting countdown")
while n > 0:
yield n
n -= 1
print("Done!")
c = count_down(3)
print(next(c)) # Starting countdown, then 3
print(next(c)) # 2
print(next(c)) # 1
print(next(c)) # Done! then raises StopIteration
The generator runs until it either yields or returns. When the function completes (or hits a return), it raises StopIteration.
A regular function that returns a list computes all values upfront:
def squares_list(n):
return [i*i for i in range(n)]
This stores the whole list in memory. A generator computes values on‑the‑fly:
def squares_gen(n):
for i in range(n):
yield i*i
The generator uses O(1) memory, independent of n.
Lazy evaluation means that values are computed only when they are needed. This provides:
Reading a huge file line by line without loading it all:
def read_large_file(file_path):
with open(file_path, 'r') as f:
for line in f:
yield line
# Processing only a few lines
for line in read_large_file('huge.log'):
if 'ERROR' in line:
print(line)
break # stops early, never reads the rest
Generators can produce infinite values:
def infinite_numbers():
n = 0
while True:
yield n
n += 1
# Take first 10
for num, _ in zip(infinite_numbers(), range(10)):
print(num)
send(), throw(), close()Generators are bidirectional; they can receive data from the caller.
send(value)Allows you to send a value back into the generator, which becomes the result of the yield expression. This enables coroutines.
def accumulator():
total = 0
while True:
x = yield total
if x is None:
break
total += x
acc = accumulator()
next(acc) # prime the generator (advance to first yield)
print(acc.send(5)) # total becomes 5, yields 5
print(acc.send(3)) # total becomes 8, yields 8
acc.close()
send() requires the generator to be started (usually with next() or send(None)).
throw(type[, value[, traceback]])Raises an exception inside the generator at the current yield point. This can be caught within the generator.
close()Stops the generator by raising GeneratorExit inside it. Used to free resources.
Generator expressions are a concise way to create generators with a syntax similar to list comprehensions, but using parentheses () instead of [].
# List comprehension: eager, creates full list
squares_list = [x*x for x in range(1000000)] # memory heavy
# Generator expression: lazy, returns a generator
squares_gen = (x*x for x in range(1000000)) # memory efficient
# Use it in a loop
for sq in squares_gen:
if sq > 1000:
break
They are ideal for large datasets where you don't need all values at once.
yield and next call has some overhead, so for small datasets, a list might be faster. For large data, memory savings often outweigh the cost.try/finally or with to ensure cleanup.def read_file_safe(filename):
with open(filename) as f:
for line in f:
yield line
# The file is closed when the generator is garbage collected or exhausted.
All generators are iterators (they implement the iterator protocol), but not all iterators are generators. A generator is created by a function with yield; a custom iterator is a class with __iter__ and __next__. Generators are simpler to write.
Which keyword is used to define a generator?
returnyieldgeneratornextWhat does a generator function return when called?
None.What happens when a generator is exhausted?
None.StopIteration.GeneratorExit.True or False: A generator can produce an infinite sequence of values.
What is the main advantage of using a generator over a list comprehension for large data?
How can you manually get the next value from a generator?
generator.next()next(generator)generator.__next__()What is a generator expression?
[].().{}.def and yield.Given gen = (x for x in range(3)), what does list(gen) return?
[0, 1, 2][0, 1, 2, 3][0, 1]What method allows you to send a value back into a generator?
send()throw()yield()return()If you call next(gen) on a generator that has just been created, where does execution start?
yield.yield.StopIteration immediately.Exercise 1: Simple Generator
Write a generator even_numbers(n) that yields even numbers from 0 up to (but not including) n.
for num in even_numbers(10): print(num) # 0,2,4,6,8
</details>
**Exercise 2: Fibonacci Generator**
Write a generator `fibonacci()` that yields Fibonacci numbers indefinitely.
<details><summary>Sample Solution</summary>
```python
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fibonacci()
for _ in range(10):
print(next(fib)) # 0,1,1,2,3,5,8,13,21,34
Exercise 3: File Reader Generator
Write a generator read_file_lines(filename) that yields each line with leading/trailing whitespace removed.
for line in read_file_lines('sample.txt'): print(line)
</details>
**Exercise 4: Generator Expression**
Given a list of numbers, use a generator expression to produce squares only for even numbers.
<details><summary>Sample Solution</summary>
```python
numbers = [1, 2, 3, 4, 5, 6]
even_squares = (x*x for x in numbers if x % 2 == 0)
for sq in even_squares:
print(sq) # 4, 16, 36
Exercise 5: Chaining Generators
Write filter_positive and square_all generators, then chain them.
def square_all(numbers): for n in numbers: yield n * n
data = [-3, -2, -1, 0, 1, 2, 3] pipeline = square_all(filter_positive(data)) print(list(pipeline)) # [1, 4, 9]
</details>
---
### 🏠 Homework – Deeper Thinking
#### Short Answer Questions
**1. Prime Number Generator**
Write a generator `primes()` that yields prime numbers indefinitely. Use it to find the 100th prime.
<details><summary>Sample Answer</summary>
```python
def primes():
yield 2
n = 3
while True:
is_prime = True
for p in range(2, int(n**0.5)+1):
if n % p == 0:
is_prime = False
break
if is_prime:
yield n
n += 2
p = primes()
for _ in range(99):
next(p)
print(next(p)) # 100th prime
2. Paginated API Data
Simulate a paginated API with get_page(page_num) and write a generator fetch_all() that yields all items.
def fetch_all(): page = 0 while True: items = get_page(page) if not items: break for item in items: yield item page += 1
print(list(fetch_all())) # [0,1,2,3,4,5]
</details>
**3. Generator with `send()`**
Write a generator `running_average()` that accepts numbers via `send()` and yields the running average.
<details><summary>Sample Answer</summary>
```python
def running_average():
total = 0
count = 0
while True:
n = yield total / count if count else 0
total += n
count += 1
avg = running_average()
next(avg)
print(avg.send(10)) # 10.0
print(avg.send(20)) # 15.0
print(avg.send(30)) # 20.0
4. Flatten Nested Iterables Recursively with Generator
Write a recursive generator flatten(iterable) that yields all elements from a deeply nested list.
print(list(flatten([1, [2, [3, 4]], 5]))) # [1,2,3,4,5]
</details>
**5. Data Pipeline with Generators**
Create a pipeline of generators that reads a log file, filters for "ERROR", extracts timestamp/message, and yields tuples.
<details><summary>Sample Answer</summary>
```python
def read_lines(filename):
with open(filename, 'r') as f:
for line in f:
yield line
def filter_errors(lines):
for line in lines:
if 'ERROR' in line:
yield line
def parse_lines(lines):
for line in lines:
# Assume format: [timestamp] message
parts = line.strip().split('] ', 1)
if len(parts) == 2:
timestamp = parts[0].lstrip('[')
message = parts[1]
yield timestamp, message
pipeline = parse_lines(filter_errors(read_lines('server.log')))
for i, (ts, msg) in enumerate(pipeline):
if i >= 10:
break
print(f"{ts}: {msg}")
primes() yields prime numbers.get_page: maintain a page counter; return list or empty. Generator: while True: page = get_page(page_num); if not page: break; for item in page: yield item; page_num += 1.def running_average():
total = 0
count = 0
while True:
n = yield total / count if count else 0
total += n
count += 1
for to chain.In this tutorial, you have learned:
yield.send(), throw(), close().Generators are a powerful tool for writing efficient, memory‑friendly Python code. They are especially useful when working with big data, streaming, or building composable processing pipelines.
Next Steps: In Tutorial 9, we will explore Decorators – a way to modify or enhance functions without changing their code.
Happy generating!