Search code examples
pythongeneratorcoroutineyieldyield-from

Trigger except clause in generator from exception raised in user code


I have a series of nested generators, and I would like to know from the first generator if an exception ocurred in the user code, for the sake of an example, consider the code below:

#############################################################################
def generator():
    try:
        for i in (1, 2, 3, 4, 5, 6):
            print(f"Generator: {i}.")
            yield i
    except:
        print("Exception handled in generator")
        raise

#############################################################################
def intermediary_generator():
    try:
        gen = generator()
        while i := gen.send(None):
            print(f"Intermediary generator: {i}.")
            yield i
    except StopIteration:
        pass
    except:
        print ("Exception handled in intermediary generator")
        raise

############################################################################
user_code_generator = intermediary_generator()
try:
    while i := user_code_generator.send(None):
        print(f"User code generator: {i}.")
        if i == 4:
            raise Exception("The exception in the user code")
except StopIteration:
    pass
except:
    print("Exception handled in user code generator")
    raise

I need the exception in the user code to propagate down to the intermediary and main generator, I was expecting the following sequence as per the print statements:

Exception handled in generator
Exception handled in intermediary generator
Exception handled in user code generator

But if I execute the code above I do not see the exception handled in the generator or intermediary generator.


Solution

  • I think I will answer my own question, the throw method of a generator does exactly what I need, which is propagating the exception in the user code down to the main generator.

    #############################################################################
    def generator():
        try:
            for i in (1, 2, 3, 4, 5, 6):
                print(f"Generator: {i}.")
                yield i
        except:
            print("Exception handled in generator")
            raise
    
    #############################################################################
    def intermediary_generator():
        try:
            gen = generator()
            while i := gen.send(None):
                print(f"Intermediary generator: {i}.")
                yield i
        except StopIteration:
            print("Stop iteration in intermediary generator")
        except Exception as exc:
            print ("Exception handled in intermediary generator")
            gen.throw(exc)
            raise
    
    ############################################################################
    user_code_generator = intermediary_generator()
    try:
        while i := user_code_generator.send(None):
            print(f"User code generator: {i}.")
            if i == 4:
                raise Exception("The exception in the user code")
    except StopIteration:
        pass
    except Exception as exc:
        print("Exception handled in user code generator")
        user_code_generator.throw(exc)
        raise
    

    And the output:

    Generator: 1.
    Intermediary generator: 1.
    User code generator: 1.
    Generator: 2.
    Intermediary generator: 2.
    User code generator: 2.
    Generator: 3.
    Intermediary generator: 3.
    User code generator: 3.
    Generator: 4.
    Intermediary generator: 4.
    User code generator: 4.
    Exception handled in user code generator
    Exception handled in intermediary generator
    Exception handled in generator
    Traceback (most recent call last):
      File "<string>", line 39, in <module>
      File "<string>", line 25, in intermediary_generator
      File "<string>", line 9, in generator
      File "<string>", line 20, in intermediary_generator
      File "<string>", line 34, in <module>
    Exception: The exception in the user code