Search code examples
pythonexceptionabstract-base-class

Catching exceptions based on their abstract base class


Suppose I've got an exception class with an abstract base class, something like this:

class MyExceptions(BaseExeption, metaclass=abc.ABCMeta):
    pass

class ProperSubclass(MyExceptions):
    pass

MyExceptions.register(ValueError)

It appears that I can catch ProperSubclass by MyExceptions, but not ValueError:

try:
    raise ProperSubclass()
except MyExceptions:
    print('As expected, control comes here...')
except:
    print('...and not here.')

try:
    raise ValueError()
except MyExceptions:
    print('Control does not come here...')
except ValueError:
    print('...but unexpectedly comes here.')

So my question is, should I be able to catch built-in exceptions by their abstract base class? If so, how? And if not, what are the rules?

I guess another way of asking this is: do except clauses properly use isinstance()/issubclass() for matching, and if not (as appears to be the case) what do they use? Perhaps there are some shady shortcuts down in the C implementation.


Solution

  • The documentation says:

    An object is compatible with an exception if it is the class or a base class of the exception object or a tuple containing an item compatible with the exception.

    Unfortunately, this doesn't say whether virtual base classes should be considered, unlike the language for e.g. issubclass:

    Return true if class is a subclass (direct, indirect or virtual) of classinfo. [...]

    The language on overriding instance and subclass checks doesn't help much either:

    The following methods are used to override the default behavior of the isinstance() and issubclass() built-in functions. [...]

    In fact, as you have suspected, the CPython implementation (for Python 3) bypasses subclass checks, calling PyType_IsSubtype directly:

    http://hg.python.org/cpython/file/3.4/Python/errors.c#l167

    PyErr_GivenExceptionMatches(PyObject *err, PyObject *exc)
    {
        ...
            /* PyObject_IsSubclass() can recurse and therefore is
               not safe (see test_bad_getattr in test.pickletester). */
            res = PyType_IsSubtype((PyTypeObject *)err, (PyTypeObject *)exc);
    

    For reference, the CPython implementation of issubclass, PyObject_IsSubclass, calls __subclasscheck__ before falling back to PyType_IsSubtype.

    So there is a good reason for this behavior; exception handling needs to be non-recursive, so it isn't safe for it to call back up into Python code. Note that the Python 2.7 version accepts the risk of overflow and does call PyObject_IsSubclass. There is a proposal to relax this restriction in Python 3, but although a patch has been written it hasn't yet been accepted. Otherwise, it would be a good idea for the documentation to clarify that except checks are non-virtual.