I’m writing a module and want to have a unified exception hierarchy for the exceptions that it can raise (e.g. inheriting from a
FooError abstract class for all the
foo module’s specific exceptions). This allows users of the module to catch those particular exceptions and handle them distinctly, if needed. But many of the exceptions raised from the module are raised because of some other exception; e.g. failing at some task because of an OSError on a file.
What I need is to “wrap” the exception caught such that it has a different type and message, so that information is available further up the propagation hierarchy by whatever catches the exception. But I don’t want to lose the existing type, message, and stack trace; that’s all useful information for someone trying to debug the problem. A top-level exception handler is no good, since I’m trying to decorate the exception before it makes its way further up the propagation stack, and the top-level handler is too late.
This is partly solved by deriving my module
foo‘s specific exception types from the existing type (e.g.
class FooPermissionError(OSError, FooError)), but that doesn’t make it any easier to wrap the existing exception instance in a new type, nor modify the message.
Python’s PEP 3134 “Exception Chaining and Embedded Tracebacks” discusses a change accepted in Python 3.0 for “chaining” exception objects, to indicate that a new exception was raised during the handling of an existing exception.
What I’m trying to do is related: I need it also working in earlier Python versions, and I need it not for chaining, but only for polymorphism. What is the right way to do this?
You could create your own exception type that extends whichever exception you’ve caught.
class NewException(CaughtException): def __init__(self, caught): self.caught = caught try: ... except CaughtException as e: ... raise NewException(e)
But most of the time, I think it would be simpler to catch the exception, handle it, and either
raise the original exception (and preserve the traceback) or
raise NewException(). If I were calling your code, and I received one of your custom exceptions, I’d expect that your code has already handled whatever exception you had to catch. Thus I don’t need to access it myself.
Edit: I found this analysis of ways to throw your own exception and keep the original exception. No pretty solutions.
You can use sys.exc_info() to get the traceback, and raise your new exception with said traceback (as the PEP mentions). If you want to preserve the old type and message, you can do so on the exception, but that’s only useful if whatever catches your exception looks for it.
import sys def failure(): try: 1/0 except ZeroDivisionError, e: type, value, traceback = sys.exc_info() raise ValueError, ("You did something wrong!", type, value), traceback
Of course, this is really not that useful. If it was, we wouldn’t need that PEP. I’d not recommend doing it.
Python 3 introduced exception chaining (as described in PEP 3134). This allows, when raising an exception, to cite an existing exception as the “cause”:
try: frobnicate() except KeyError as exc: raise ValueError("Bad grape") from exc
The caught exception (
exc, a KeyError) thereby becomes part of (is the “cause of”) the new exception, a ValueError. The “cause” is available to whatever code catches the new exception.
By using this feature, the
__cause__ attribute is set. The built-in exception handler also knows how to report the exception’s “cause” and “context” along with the traceback.
The most straighforward solution to your needs should be this:
try: upload(file_id) except Exception as upload_error: error_msg = "Your upload failed! File: " + file_id raise RuntimeError(error_msg, upload_error)
In this way you can later print your message and the specific error throwed by the upload function
I also found that many times i need some “wrapping” to errors raised.
This included both in a function scope and sometimes wrap only some lines inside a function.
Created a wrapper to be used a
import inspect from contextlib import contextmanager, ContextDecorator import functools class wrap_exceptions(ContextDecorator): def __init__(self, wrapper_exc, *wrapped_exc): self.wrapper_exc = wrapper_exc self.wrapped_exc = wrapped_exc def __enter__(self): pass def __exit__(self, exc_type, exc_val, exc_tb): if not exc_type: return try: raise exc_val except self.wrapped_exc: raise self.wrapper_exc from exc_val def __gen_wrapper(self, f, *args, **kwargs): with self: for res in f(*args, **kwargs): yield res def __call__(self, f): @functools.wraps(f) def wrapper(*args, **kw): with self: if inspect.isgeneratorfunction(f): return self.__gen_wrapper(f, *args, **kw) else: return f(*args, **kw) return wrapper
@wrap_exceptions(MyError, IndexError) def do(): pass
do method, don’t worry about
try: do() except MyError as my_err: pass # handle error
def do2(): print('do2') with wrap_exceptions(MyError, IndexError): do()
do2, in the
context manager, if
IndexError is raised, it will
be wrapped and raised
This is tangential, but when building consistent error messages for my own library, I found we were wrapping our own error messages as they percolated up. Was pleased to see that Python 3.11 now offers an add_note function to augment an existing error with additional information, which might also be useful.
For Python 3.11
The add_note() method is added to BaseException. It can be used to
enrich exceptions with context information that is not available at
the time when the exception is raised. The added notes appear in the
So we are now following this pattern:
try: some_risky_business() except MyCustomException as ce: ce.add_note(f"Here is some more critical context!") raise se except Exception as e: raise MyCustomException("Wow, didn't expect this error.") from e
To really "convert" the exception and avoid a context or cause as explained by @bignose’s answer, you have to do some hoakey stuff (Python 3 below):
import sys new_ex = None try: something_that_raises_ValueError() except ValueError: _, _, tb = sys.exc_info() new_ex = TypeError('This is really how I want to report this') if new_ex is not None: raise new_ex.with_traceback(tb)
By passing in the traceback, you keep it pointing to where the problem occurred, not your
This could probably be turned into a context to be made more reusable.
Note that if you only want to change the message, you can manipulate the
args. I have these two functions for that:
def append_message(e_: Exception, msg: str): """ Appends `msg` to the message text of the exception `e`. Parameters ---------- e_: Exception An exception instance whose `args` will be modified to include `msg`. msg: str The string to append to the message. """ if e_.args: # Exception was raised with arguments e_.args = (str(e_.args) + msg,) + e_.args[1:] else: e_.args = (msg,) def replace_message(e_: Exception, msg: str): """ Replaces the exception message with `msg`. Parameters ---------- e_: Exception An exception instance whose `args` will be modified to be `msg`. msg: str The string to replace the exception message with. """ if e_.args: e_.args = (msg,) + e_.args[1:] else: e_.args = (msg,)