Search code examples
pythonexceptionpython-internals

Why do GeneratorExit and StopIteration have different base classes?


I was taking a look at the hierarchy of the built-in python exceptions, and I noticed that StopIteration and GeneratorExit have different base classes:

BaseException
 +-- SystemExit
 +-- KeyboardInterrupt
 +-- GeneratorExit
 +-- Exception
      +-- StopIteration
      +-- StandardError
      +-- Warning

Or in code:

>>> GeneratorExit.__bases__
(<type 'exceptions.BaseException'>,)
>>> StopIteration.__bases__
(<type 'exceptions.Exception'>,)

When I go to the specific description of each exception, I can read following:

https://docs.python.org/2/library/exceptions.html#exceptions.GeneratorExit

exception GeneratorExit

Raised when a generator‘s close() method is called. It directly inherits from BaseException instead of StandardError since it is technically not an error.

https://docs.python.org/2/library/exceptions.html#exceptions.StopIteration

exception StopIteration

Raised by an iterator‘s next() method to signal that there are no further values. This is derived from Exception rather than StandardError, since this is not considered an error in its normal application.

Which is not very clear to me. Both are similar in the sense that they do not notify errors, but an "event" to change the flow of the code. So, they are not technically errors, and I understand that they should be separated from the rest of the exceptions... but why is one a subclass of BaseException and the other one a subclass of Exception?.

In general I considered always that Exception subclasses are errors, and when I write a blind try: except: (for instance calling third party code), I always tried to catch Exception, but maybe that is wrong and I should be catching StandardError.


Solution

  • It is quite common to use try: ... except Exception: ... blocks.

    If GeneratorExit would inherit from Exception you would get the following issue:

    def get_next_element(alist):
        for element in alist:
            try:
                yield element
            except BaseException:  # except Exception
                pass
    
    for element in get_next_element([0,1,2,3,4,5,6,7,8,9]):
        if element == 3:
            break
        else:
            print(element)
    
    0
    1
    2
    Exception ignored in: <generator object get_next_element at 0x7fffed7e8360>
    RuntimeError: generator ignored GeneratorExit
    

    This example is quite simple but imagine in the try block a more complex operation which, in case of failure, would simply ignore the issue (or print a message) and get to the next iteration.

    If you would catch the generic Exception, you would end up preventing the user of your generator from breaking the loop without getting a RuntimeError.

    A better explanation is here.

    EDIT: answering here as it was too long for a comment.

    I'd rather say the opposite. GeneratorExit should inherit from Exception rather than BaseException. When you catch Exception you basically want to catch almost everything. BaseException as PEP-352 states, is for those exceptions which need to be "excepted" in order to allow the user to escape from code that would otherwise catch them. In this way you can, for example, still CTRL-C running code. GeneratorExit falls into that category in order to break loops. An interesting conversation about it on comp.lang.python.