exceptional

exceptional is a small Python library providing various exception handling utilities.

Installation

pip install exceptional

exceptional supports reasonably modern Python versions, which at the time of writing means Python 3.6+.

Usage

All functionality is available from the exceptional module:

import exceptional

Suppressing exceptions

The exceptional.suppress() function can be used as a context manager as a succinct way to suppress one or more exceptions:

with exceptional.suppress(FileNotFoundError):
    os.remove(...)

This is similar to simply ignoring exceptions like this, but shorter:

try:
    os.remove(...)
except FileNotFoundError:
  pass

This behaves the same as the contextlib.suppress() from the standard library, but this implementation can also be used as a decorator:

@exceptional.suppress(ValueError, LookupError):
def do_something():
    ...

Collecting exceptions

The exceptional.collect() function can be used to collect exceptions. This is similar to exceptional.suppress(), but remembers the raised exceptions:

collector = exceptional.collect(ValueError, TypeError)
for item in ...:
    with collector:
        do_something(item)

To access the collected exceptions, iterate over the collector:

for exc in collector:
    print(exc)

Note that exceptional.collect() cannot be used as a decorator, since there is no sensible way to obtain the results, and it could result in an ever-growing list of collected exceptions which cannot be cleared.

Wrapping exceptions

The exceptional.wrap() function can be used to catch an exception (or multiple exceptions), and raise a replacement exception in its place. This should be used with care, but can be useful to (partially) hide exceptions from underlying libraries by converting them to application-specific exceptions.

By default, the original exception is set as the direct cause of the new exception, using a mechanism that is equivalent to a statement of the form:

raise NewException(...) from original

In practice, this means that the Python interpreter will show both exceptions (including tracebacks) when the exception causes the application to crash. For more information, see the Python documentation about exceptions and the raise statement.

The examples below show basic usage.

  • A context manager can be a convenient replacement for a longer try/except/raise from combination:

    class ConfigurationError(Exception):
        pass
    
    def load_configuration(config_file):
        with exceptional.wrap(FileNotFoundError, ConfigurationError)
            with open(config_file, 'r') as fp:
                return json.load(fp)
    
  • A decorator achieves similar behaviour for a complete function:

    @exceptional.wrap(FileNotFoundError, ConfigurationError)
    def load_configuration(config_file):
        with open(config_file, 'r') as fp:
            return json.load(fp)
    

The above examples replace a single exception with another one. Handling multiple exceptions can be done in two ways:

  • provide a tuple, just like a regular except statement:

    with exceptional.wrap((ValueError, KeyError), CustomException):
        ...
    
  • provide a mapping, which allows for more complex configuration, and also correctly handles exception hierarchies:

    mapping = {
        ValueError: CustomException,
        KeyError: AnotherCustomException,
    }
    with exceptional.wrap(mapping):
        ...
    

By default the message for the first exception is reused for the replacement exception. For more control over the message, use one of the following (keyword-only) arguments.

  • To provide a new message, use the message argument:

    with exceptional.wrap(ValueError, CustomException, message="oops"):
        ...
    
  • Pass None to remove the message altogether:

    with exceptional.wrap(ValueError, CustomException, message=None):
        ...
    
  • To add another message in front of the original message, use prefix:

    with exceptional.wrap(ValueError, CustomException, prefix="oops"):
        raise ValueError("foo")
    

    The above example results in CustomException("oops: foo").

  • For more flexibility, use format and use a {} placeholder:

    template = "Something went wrong. Likely reason: {}. Please file a bug."
    with exceptional.wrap(ValueError, CustomException, format=template):
        ...
    

Finally, for more control over the exception context and cause, use the set_cause and suppress_context args:

with exceptional.wrap(ValueError, CustomException, set_cause=False):
    ...

with exceptional.wrap(ValueError, CustomException, suppress_context=True):
    ...

Creating exception-raising callables

The exceptional.raiser() function can be used to quickly create a callable that raises an exception when called. This can be useful to create callback functions that should do nothing besides immediately raise an exception:

callback = raiser(RuntimeError, "that took way too long")

Any arguments passed to this function, such as the error message above, will be passed along to the exception’s constructor. The returned callable can now be used in callback-based APIs, for example with an asyncio event loop:

loop = ...
loop.call_later(delay, callback)
loop.run_forever()

The above cannot be done using a lambda expression, since raise is a statement, and statements cannot be used inside lambda expressions.

The callable returned by exceptional.raiser() will accept any arguments and ignore those, making it suitable for use as a callback, regardless of the expected signature.

API

exceptional.suppress(*exceptions) → exceptional.exceptional.ExceptionSuppressor

Suppress the specified exception(s).

This can be used as a context manager or as a decorator.

exceptional.collect(*exceptions) → exceptional.exceptional.ExceptionCollector

Create a collector for the specified exception(s).

This can be used as a context manager. Iterate over the returned collector object to access the collected exceptions.

exceptional.wrap(original_or_mapping: Union[Mapping[Union[Type[BaseException], Sequence[Type[BaseException]]], Type[BaseException]], Type[BaseException]], replacement: Optional[Type[BaseException]] = None, *, message: Union[exceptional.exceptional.Missing, str, None] = <MISSING>, prefix: Optional[str] = None, format: Optional[str] = None, set_cause: bool = True, suppress_context: bool = False) → exceptional.exceptional.ExceptionWrapper

Raise a replacement exception wrapping the original exception.

This can be used as a context manager or as a decorator.

exceptional.raiser(exception: Type[BaseException] = <class 'Exception'>, *args, **kwargs) → exceptional.exceptional.ExceptionRaiser

Create a callable that will immediately raise exception when called.

Arguments, if any, will be passed along to the exception’s constructor.

Contributing

The source code and issue tracker for this package can be found on Github:

Version history

  • 2.0.0 (2019-03-16)
    • Drop Python 3.4 and 3.5 support
    • Add type annotations and enable strict checking using mypy
    • Use __qualname__ in various __repr__() functions
    • Various cleanups
  • 1.0.0 (2019-03-03)
    • Initial release

License

Copyright © 2014–2019, wouter bolsterlee

All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  • Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  • Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  • Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

(This is the OSI approved 3-clause “New BSD License”.)

Embedded Python standard library code

Some parts of this library contain (slightly modified) pieces of code extracted from the Python standard library; refer to the official Python documentation for license information for Python itself.