Sometimes exception handling can obscure bugs unless you guard against a particular exception occurring in a certain place. One example is that accidentally raising StopIteration
inside a generator will halt the generator instead of displaying a traceback. That was solved by PEP 479, which automatically has such StopIteration
exceptions change to RuntimeError
. See the discussion below for further examples.
Here is a class which can be used as either a decorator or context manager for guarding against the given exceptions. It takes an exception (or a tuple of exceptions) as argument, and if the wrapped code raises that exception, it is re-raised as another exception type (by default RuntimeError
).
For example:
try:
with exception_guard(ZeroDivisionError):
1/0 # raises ZeroDivisionError
except RuntimeError:
print ('ZeroDivisionError replaced by RuntimeError')
@exception_guard(KeyError)
def demo():
return {}['key'] # raises KeyError
try:
demo()
except RuntimeError:
print ('KeyError replaced by RuntimeError')
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | from functools import wraps
class exception_guard(object):
"""Guard against the given exception and raise a different exception."""
def __init__(self, catchable, throwable=RuntimeError):
if is_exception_class(catchable):
self._catchable = catchable
else:
raise TypeError('catchable must be one or more exception types')
if throwable is None or is_exception(throwable):
self._throwable = throwable
else:
raise TypeError('throwable must be None or an exception')
def throw(self, cause):
"""Throw an exception from the given cause."""
throwable = self._throwable
assert throwable is not None
self._raisefrom(throwable, cause)
def _raisefrom(self, exception, cause):
# "raise ... from ..." syntax only supported in Python 3.
assert cause is not None # "raise ... from None" is not supported.
if isinstance(exception, BaseException):
# We're given an exception instance, so just use it as-is.
pass
else:
# We're given an exception class, so instantiate it with a
# helpful error message.
assert issubclass(exception, BaseException)
name = type(cause).__name__
message = 'guard triggered by %s exception' % name
exception = exception(message)
try:
exec("raise exception from cause", globals(), locals())
except SyntaxError:
# Python too old. Fall back to a simple raise, without cause.
raise exception
# === Context manager special methods ===
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
if exc_type is not None and issubclass(exc_type, self._catchable):
if self._throwable is None:
# Suppress the exception.
return True
else:
self.throw(exc_value)
# === Use exception_guard as a decorator ===
def __call__(self, function):
catchable = self._catchable
suppress_exception = (self._throwable is None)
@wraps(function)
def inner(*args, **kwargs):
try:
result = function(*args, **kwargs)
except catchable as error:
if suppress_exception:
return
else:
self.throw(error)
else:
return result
return inner
# Two helper functions.
def is_exception(obj):
"""Return whether obj is an exception.
>>> is_exception(ValueError) # An exception class.
True
>>> is_exception(ValueError()) # An exception instance.
True
>>> is_exception(float)
False
"""
try:
return issubclass(obj, BaseException)
except TypeError:
return isinstance(obj, BaseException)
def is_exception_class(obj):
"""Return whether obj is an exception class, or a tuple of the same.
>>> is_exception_class(ValueError)
True
>>> is_exception_class(float)
False
>>> is_exception_class(ValueError()) # An instance, not a class.
False
>>> is_exception_class((ValueError, KeyError))
True
"""
try:
if isinstance(obj, tuple):
return obj and all(issubclass(X, BaseException) for X in obj)
return issubclass(obj, BaseException)
except TypeError:
return False
|
Another motivating example for this is the use of AttributeError
, especially when it interacts with properties or getattr
. If the property
getter or setter raises AttributeError
due to a bug, that may be interpreted as the property not existing at all. Similarly:
class X(object):
_data = 1
@property
def value(self):
return self._daat + 1 # oops misspelled
hasattr(X(), 'value') # should return True, but returns False
These problems with exception-handling code have been discussed in these two threads on the Python-Ideas mailing list:
- "the error that raises an AttributeError should be passed to __getattr__"
- "Improving Catching Exceptions"
The exception_guard
class will accept either a single exception class, or a tuple of exception classes, for the exception to be guarded against. For the replacement exception, you can pass either an exception class, an exception instance, or None
. If you pass None
, the exception will be suppressed. If you don't specify a replacement, RuntimeError
is used.
When used as a decorator, suppressing the caught exception results in the function returning None
.
There is one minor gotcha when used as a context manager: the exception is only replaced if you allow it to bubble out of the with
block. Inside the with
block, the original exception is still visible:
try:
with exception_guard(AttributeError):
try:
"string".uper() # misspelled
except AttributeError:
print("caught AttributeError inside block")
raise
except RuntimeError:
print ("AttributeError replaced by RuntimeError")
This has been tested with Python 2.6, 2.7, 3.3 and 3.5, but should work for any version of Python that supports the except X as name
syntax.
See also: http://bugs.python.org/issue30792