Welcome, guest | Sign In | My Account | Store | Cart

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')
Python, 109 lines
  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 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.

1 comment