[Python-ideas] Use `isinstance` check during exception handling
sjoerdjob at sjec.nl
sjoerdjob at sjec.nl
Wed Nov 4 17:02:41 EST 2015
Hi all,
TL;DR: Change exception checking logic from
err.__class__ in exc.__mro__
to
isinstance(err, exc)
Because it is more powerful, more consistent and allows for cleaner
code.
The reason I am proposing this change is that quite often, one has to
deal with libraries behaving 'badly' with regard to exception
granularity.
I will take the Python base library before PEP3151 [1] as an example. It
is not hard to imagine there are a lot of other cases of libraries that
behave with little granularity in the exception classes.
try:
some(operation)
except OSError as err:
if err.errno == errno.ENOENT:
# handle file-not-exists
elif err.errno == errno.EACCES:
# handle permission stuff.
else:
raise
After PEP3151 one can now write:
try:
some(operation)
except FileNotFoundError:
# handle file-not-exists
except PermissionError:
# handle permission stuff.
Now, PEP3151 took over a year to be resolved, judging by the dates. If
we have to go through changes like this more often, one will have to
wait quite a while. Also, consider backwards compatibility issues. Code
using FileNotFoundError will only run on Python3.3+.
Thus I propose to have the exception handling code use the normal
`isinstance` machinery, as it is more powerful, and allows one to use
virtual base classes to check if the instance of the exception is
relevant to catch. An example:
import errno
# Fake `FileNotFoundError` in Python3.2
class ENOENTCheck(type):
def __instancecheck__(cls, inst):
return isinstance(inst, EnvironmentError) and \
inst.errno == errno.ENOENT
class FileNotFoundError(EnvironmentError, metaclass=ENOENTCheck):
pass
try:
open("no-such-file")
except Exception as e:
print(isinstance(e, FileNotFoundError))
This would print `True`. But, using `FileNotFoundError` as a class to
catch will not work, because the `__mro__` is checked instead.
I proposed the change on the Python issue tracker [2] already, but it
was (rightfully, I think) decided that it needs some more discussion
because it will bring a (potentially huge) semantic change.
As an opening, here are some pros and cons I can think of:
Cons:
- *Code already written might break*: Highly unlikely. If you have
written code that overrides `isinstance` logic, you'd more than likely
not write code that depends on there being a difference in matching
logic between the exception handling and normal `isinstance` calls.
- *Performance will go down*: Yes, instance-checking is probably a lot
more expensive then subclass-checking. I haven't made the change and
benchmarked it yet, but based on Martin Panter's comment [3], it might
only cost a lot when actually checking against a virtual base class.
If cost is limited to those cases, I think the cost is negligible.
- *Exception handling like this is just magic*: Yes, anything regarding
the metaclasses might be considered as magic. But metaclasses also
allow you to build great abstractions, like for instance the ORM
included with Django. I think this is one of the cases where the
benefits outweigh the cost.
Pros:
- *Cleaner code*: The same benefit PEP 3151 brought for the base library
and OS errors can be extended to all the Python libraries that lack
granularity.
- *Consistency*: Who would ever expect that you can not always catch an
exception `foo` using class `Bar` when `isinstance(foo, Bar)`.
Even though I can currently think of more downsides than upsides, I do
think that the downsides are really minor, while the upsides are huge:
clean code and consistency.
Also, I set up a proof-of-concept PEP3151 compatibility layer for
Python2.7 that sort-of fakes this feature. It fakes it by checking the
instance `sys.exc_info()[1]` in the `__subclasscheck__`. For the proof
of concept see [4]. I only found out during porting to Python3 that it
does not actually work, because of the direct checking of the `__mro__`,
but that's hopefully going to be fixed soon-ish [5].
I'd be interested to hear your thoughts about this. Any more pros and
cons would be welcome, as well as your judgement as to whether the
breaking of backwards compatibility is potentially a bad idea.
Kind regards,
Sjoerd Job Postmus
[1] PEP 3151 -- Reworking the OS and IO exception hierarchy
https://www.python.org/dev/peps/pep-3151/
[2] Call `isinstance` instead of `issubclass` during exception handling
http://bugs.python.org/issue25537
[3] http://bugs.python.org/issue25537#msg253963
[4] https://github.com/sjoerdjob/exhacktion/
[5] Catching virtual subclasses in except clauses
http://bugs.python.org/issue12029
More information about the Python-ideas
mailing list