Python Exception Chaining

Python exception chaining

Exception chaining is only available in Python 3, where you can write:

try:
v = {}['a']
except KeyError as e:
raise ValueError('failed') from e

which yields an output like

Traceback (most recent call last):
File "t.py", line 2, in <module>
v = {}['a']
KeyError: 'a'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
File "t.py", line 4, in <module>
raise ValueError('failed') from e
ValueError: failed

In most cases, you don't even need the from; Python 3 will by default show all exceptions that occured during exception handling, like this:

Traceback (most recent call last):
File "t.py", line 2, in <module>
v = {}['a']
KeyError: 'a'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "t.py", line 4, in <module>
raise ValueError('failed')
ValueError: failed

What you can do in Python 2 is adding custom attributes to your exception class, like:

class MyError(Exception):
def __init__(self, message, cause):
super(MyError, self).__init__(message + u', caused by ' + repr(cause))
self.cause = cause

try:
v = {}['a']
except KeyError as e:
raise MyError('failed', e)

What is the reason to use exception chaining

In an exception handler, if you explicitly raise an exception you'll either want to use from err or from None (depending on if it makes more sense to suppress the original traceback or not).

Without from, the message you get: "during handling of the above exception, another exception occurred", suggests "something went wrong, and then, while trying to recover from that, something else (possibly unrelated) went wrong. This is the case when you, for example make a typo (my_logger.expeption("Error occurred")) or you try to save something to a file which can't be opened.

The with from, it suggests that only a single thing went wrong, that you're not handling yourself but you have some more information that is of use to the programmer or user. For example, you can catch a KeyError and raise a different exception with the message "tried to edit a non-existing widget" or whatever.

So raise SomeException("some description") from err is a way to be able to explain what when wrong if the code that raised err can't do that (because it is from the standard library or because it otherwise lacks the necessary context in which it is called), without having the user thing that two separate things went wrong right after each other.

Disable exception chaining in python 3

Simple Answer

try:
print(10/0)
except ZeroDivisionError as e:
raise AssertionError(str(e)) from None

However, you probably actually want:

try:
print(10/0)
except ZeroDivisionError as e:
raise AssertionError(str(e)) from e

Explanation

__cause__

Implicit exception chaining happens through __context__ when there isn't an explicit cause exception set.

Explicit exception chaining works through __cause__ so if you set __cause__ to the exception itself, it should stop the chaining. If __cause__ is set, Python will suppress the implicit message.

try:
print(10/0)
except ZeroDivisionError as e:
exc = AssertionError(str(e))
exc.__cause__ = exc
raise exc

raise from

We can use "raise from" to do the same thing:

try:
print(10/0)
except ZeroDivisionError as e:
exc = AssertionError(str(e))
raise exc from exc

None __cause__

Setting __cause__ to None actually does the same thing:

try:
print(10/0)
except ZeroDivisionError as e:
exc = AssertionError(str(e))
exc.__cause__ = None
raise exc

raise from None

So that brings us to the most elegant way to do this which is to raise from None:

try:
print(10/0)
except ZeroDivisionError as e:
raise AssertionError(str(e)) from None

But I would argue that you usually want to explicitly raise your exception from the cause exception so the traceback is preserved:

try:
print(10/0)
except ZeroDivisionError as e:
raise AssertionError(str(e)) from e

This will give us a slightly different message that states that the first exception was the direct cause of the second:

Traceback (most recent call last):
File "<stdin>", line 2, in <module>
ZeroDivisionError: division by zero

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
File "<stdin>", line 4, in <module>
AssertionError: division by zero

How do you raise an exception inside an except block without exception chaining in a way that's compatible with both Python 2 and 3?

The following produces identical output on py2.7.13 and py3.9.2, if you call it with the full path. (If you use a relative path, py3 expands it, but py2 does not.)

try:
1/0
except ZeroDivisionError:
internal = Exception("Error!")
internal.__suppress_context__ = True
raise internal

Traceback (most recent call last):

File "b:\development\python\so.py", line 6, in

raise internal

Exception: Error!

I was hoping to avoid internal attributes that might change, but __suppress_context__ is documented https://docs.python.org/3/library/exceptions.html

unit test for custom exception (chained exception python 3)

from client import MyCustomException

def testExceptionCase(self):
with self.assertRaises(MyCustomException):
self.someFunc()

Also you may want to use UpperCamelCase for Exceptions and Classes in general

Python: Correct way to handle chain exceptions

Probably use a loop?

def fun(i):
errors = []
things_to_try = (thing1, thing2, thing3, thing4)
for thing in things_to_try:
try:
thing(i)
except Exception as e:
errors.append(e)
else:
break
else:
raise Exception("Failed: %s" % errors)


Related Topics



Leave a reply



Submit