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
Equivalent of Numpy.Argsort() in Basic Python
Python: Open File in Zip Without Temporarily Extracting It
How to Make a Barplot and a Lineplot in the Same Seaborn Plot with Different Y Axes Nicely
Dataframe Set_Index Not Setting
How to Redirect Print Statements to Tkinter Text Widget
How to Access a Dictionary Key Value Present Inside a List
Adding Labels in X Y Scatter Plot with Seaborn
Is There a Simple Way to Change a Column of Yes/No to 1/0 in a Pandas Dataframe
How to Extract Text and Text Coordinates from a PDF File
Why Do Attribute References Act Like This with Python Inheritance
How to Write Tests for the Argparse Portion of a Python Module
Instance Attribute Attribute_Name Defined Outside _Init_
How to Invoke Pandas.Rolling.Apply with Parameters from Multiple Column