Search code examples
pythonexceptiongenerator

Why does generator raise an exception when a loop that iterates over it is interrupted?


def gen():
    try:
        yield 1
        yield 2
    except:
        print('hi')
def func():
    for x in gen():
        return x
print(func())

This code prints hi and then prints 1. Why doesn't it just print 1? What exception was raised?


Solution

  • Your func retrieves the first element from the gen generator. After that, func has a return statement. As a result, func execution terminates, causing the gen generator to exit as well. When a generator's close method is called, it raises a GeneratorExit exception, which is the exception caught in this case. That is why it prints hi. After that, program continues; func returns 1 and this value is printed in the final line of code.

    The generator.close method is invoked because the generator dies. After your for loop, there are no remaining references to the generator object. If you assign the generator to a variable a = gen() and you don't fully consume its iteration, you can trigger the raising of GeneratorExit by directly calling a.close() or by killing the object using del a.

    On the other hand if you allow your generator to complete its execution, it will not raise GeneratorExit. When the loop for _ in gen(): pass completes, it successfully concludes the execution of the gen generator, resulting in a StopIteration exception that is handled by the for loop. But it will not raise GeneratorExit because, by the time close is called, the execution of gen has already concluded.


    It is interesting that GeneratorExit does not inherit from Exception but from BaseException. If you were to write except Exception: print('hi'), only 1 would be printed. By so, you can manage any exception raised in your generator without interrupting its execution:

    def gen():
        for i in (1, 2):
            try:
                yield i
            except Exception:
                pass
    

    If you were to use except: pass instead, it would catch GeneratorExit and permit the execution of gen to continue yielding another value. Which should not happen according to generator.close documentation:

    If the generator yields a value, a RuntimeError is raised.

    In fact, you would encounter a RuntimeError: generator ignored GeneratorExit