4.9. Avoid Causing State Transitions in Generators with throw

In addition to yield from expressions (see Item 33: “Compose Multiple Generators with yield from”) and the send method (see Item 34: “Avoid Injecting Data into Generators with send”), another advanced generator feature is the throw method for re-raising Exception instances within generator functions. The way throw works is simple: When the method is called, the next occurrence of a yield expression re-raises the provided Exception instance after its output is received instead of continuing normally. Here, I show a simple example of this behavior in action:

>>> class MyError(Exception):
>>>     pass
def my_generator():

yield 1 yield 2 yield 3

it = my_generator() print(next(it)) # Yield 1 print(next(it)) # Yield 2 print(it.throw(MyError('test error')))

>>>
1
2
Traceback ...
MyError: test error

When you call throw, the generator function may catch the injected exception with a standard try/except compound statement that surrounds the last yield expression that was executed (see Item 65: “Take Advantage of Each Block in try/except/else/finally” for more about exception handling):

>>> def my_generator():
>>>     yield 1
>>>
>>>     try:
>>>         yield 2
>>>     except MyError:
>>>         print('Got MyError!')
>>>     else:
>>>         yield 3
>>>
>>>     yield 4
>>>
>>> it = my_generator()
>>> print(next(it)) # Yield 1
>>> print(next(it)) # Yield 2
>>> print(it.throw(MyError('test error')))
1
2
Got MyError!
4

This functionality provides a two-way communication channel between a generator and its caller that can be useful in certain situations (see Item 34: “Avoid Injecting Data into Generators with send” for another one). For example, imagine that I’m trying to write a program with a timer that supports sporadic resets. Here, I implement this behavior by defining a generator that relies on the throw method:

>>> class Reset(Exception):
>>>     pass
>>>
>>> def timer(period):
>>>     current = period
>>>     while current:
>>>         current -= 1
>>>         try:
>>>             yield current
>>>         except Reset:
>>>             current = period

In this code, whenever the Reset exception is raised by the yield expression, the counter resets itself to its original period.

I can connect this counter reset event to an external input that’s polled every second. Then, I can define a run function to drive the timer generator, which injects exceptions with throw to cause resets, or calls announce for each generator output:

>>> def check_for_reset():
>>>     # Poll for external event
>>>     ...
>>>
>>> def announce(remaining):
>>>     print(f'{remaining} ticks remaining')
>>>
>>> def run():
>>>     it = timer(4)
>>>     while True:
>>>         try:
>>>             if check_for_reset():
>>>                 current = it.throw(Reset())
>>>             else:
>>>                 current = next(it)
>>>         except StopIteration:
>>>             break
>>>         else:
>>>             announce(current)
>>>
>>> run()
3 ticks remaining
2 ticks remaining
1 ticks remaining
0 ticks remaining

This code works as expected, but it’s much harder to read than necessary. The various levels of nesting required to catch StopIteration exceptions or decide to throw, call next, or announce make the code noisy.

A simpler approach to implementing this functionality is to define a stateful closure (see Item 38: “Accept Functions Instead of Classes for Simple Interfaces”) using an iterable container object (see Item 31: “Be Defensive When Iterating Over Arguments”). Here, I redefine the timer generator by using such a class:

>>> class Timer:
>>>     def __init__(self, period):
>>>         self.current = period
>>>         self.period = period
>>>
>>>     def reset(self):
>>>         self.current = self.period
>>>
>>>     def __iter__(self):
>>>         while self.current:
>>>             self.current -= 1
>>>             yield self.current

Now, the run method can do a much simpler iteration by using a for statement, and the code is much easier to follow because of the reduction in the levels of nesting:

>>> def run():
>>>     timer = Timer(4)
>>>     for current in timer:
>>>         if check_for_reset():
>>>             timer.reset()
>>>         announce(current)
>>>
>>> run()
3 ticks remaining
2 ticks remaining
1 ticks remaining
0 ticks remaining

The output matches the earlier version using throw, but this implementation is much easier to understand, especially for new readers of the code. Often, what you’re trying to accomplish by mixing generators and exceptions is better achieved with asynchronous features (see Item 60: “Achieve Highly Concurrent I/O with Coroutines”). Thus, I suggest that you avoid using throw entirely and instead use an iterable class if you need this type of exceptional behavior.

4.9.1. Things to Remember

✦ The throw method can be used to re-raise exceptions within generators at the position of the most recently executed yield expression.

✦ Using throw harms readability because it requires additional nesting and boilerplate in order to raise and catch exceptions.

✦ A better way to provide exceptional behavior in generators is to use a class that implements the __iter__ method along with methods to cause exceptional state transitions.