Why does raising an exception invoke __subclasscheck__?

Viewed 199

Consider the following example that uses __subclasscheck__ for a custom exception type:

class MyMeta(type):
    def __subclasscheck__(self, subclass):
        print(f'__subclasscheck__({self!r}, {subclass!r})')

class MyError(Exception, metaclass=MyMeta):
    pass

Now when raising an exception of this type, the __subclasscheck__ method gets invoked; i.e. raise MyError() results in:

__subclasscheck__(<class '__main__.MyError'>, <class '__main__.MyError'>)
Traceback (most recent call last):
  File "test.py", line 8, in <module>
    raise MyError()
__main__.MyError

Here the first line of the output shows that __subclasscheck__ got invoked to check whether MyError is a subclass of itself, i.e. issubclass(MyError, MyError). I'd like to understand why that's necessary and how it's useful in general.


I'm using CPython 3.8.1 to reproduce this behavior. I also tried PyPy3 (3.6.9) and here __subclasscheck__ is not invoked.

1 Answers

I guess this is a CPython implementation detail. As stated in documentation to PyErr_NormalizeException:

Under certain circumstances, the values returned by PyErr_Fetch() below can be “unnormalized”, meaning that *exc is a class object but *val is not an instance of the same class.

So sometime during the processing of the raised error, CPython will normalize the exception, because otherwise it cannot assume that the value of the error is of the right type.

In your case it happens as follows:

  • Eventually while processing the exception, PyErr_Print is called, where it calls _PyErr_NormalizeException.
  • _PyErr_NormaliizeException calls PyObject_IsSubclass.
  • PyObject_IsSubclass uses __subclasscheck__ if it is provided.

I cannot say what those "certain circumstances" for "*exc is a class object but *val is not an instance of the same class" are (maybe needed for backward compatibility - I don't know).


My first assumption was, that it happens, when CPython ensures (i.e. here), that the exception is derived from BaseException.

The following code

class OldStyle():
    pass

raise OldStyle

would raise OldStyle for Python2, but TypeError: exceptions must be old-style classes or derived from BaseException, not type for

class NewStyle(object):
    pass

raise NewStyle

or TypeError: exceptions must derive from BaseException in Python3 because in Python3 all classes are "new style".

However, for this check not PyObject_IsSubclass but PyType_FastSubclass is used:

#define PyExceptionClass_Check(x)                                       \
    (PyType_Check((x)) &&                                               \
     PyType_FastSubclass((PyTypeObject*)(x), Py_TPFLAGS_BASE_EXC_SUBCLASS))

i.e. only the tpflags are looked at.

Related