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.
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()
andissubclass()
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.