Python "raise from" usage
--
Music by Eric Matyas
https://www.soundimage.org
Track title: Flying Over Ancient Lands
--
Chapters
00:00 Question
00:53 Accepted answer (Score 426)
03:49 Answer 2 (Score 13)
09:58 Answer 3 (Score 1)
14:04 Thank you
--
Full question
https://stackoverflow.com/questions/2475...
Accepted answer links:
[raise]: https://docs.python.org/3/reference/simp...
[Built-in Exceptions documentation]: https://docs.python.org/3/library/except...
Answer 2 links:
[PEP 3134, Exception Chaining and Embedded Tracebacks]: https://peps.python.org/pep-3134/
[PEP 415, Implement Context Suppression with Exception Attributes]: https://peps.python.org/pep-0415/
Answer 3 links:
[PEP-3134]: https://legacy.python.org/dev/peps/pep-3.../
--
Content licensed under CC BY-SA
https://meta.stackexchange.com/help/lice...
--
Tags
#python #python3x #syntax #exceptionhandling
#avk47
ACCEPTED ANSWER
Score 517
The difference is that when you use from, the __cause__ attribute is set and the message states that the exception was directly caused by. If you omit the from then no __cause__ is set, but the __context__ attribute may be set as well, and the traceback then shows the context as during handling something else happened.
Setting the __context__ happens if you used raise in an exception handler; if you used raise anywhere else no __context__ is set either.
If a __cause__ is set, a __suppress_context__ = True flag is also set on the exception; when __suppress_context__ is set to True, the __context__ is ignored when printing a traceback.
When raising from a exception handler where you don't want to show the context (don't want a during handling another exception happened message), then use raise ... from None to set __suppress_context__ to True.
In other words, Python sets a context on exceptions so you can introspect where an exception was raised, letting you see if another exception was replaced by it. You can also add a cause to an exception, making the traceback explicit about the other exception (use different wording), and the context is ignored (but can still be introspected when debugging). Using raise ... from None lets you suppress the context being printed.
See the raise statement documenation:
The
fromclause is used for exception chaining: if given, the second expression must be another exception class or instance, which will then be attached to the raised exception as the__cause__attribute (which is writable). If the raised exception is not handled, both exceptions will be printed:>>> try: ... print(1 / 0) ... except Exception as exc: ... raise RuntimeError("Something bad happened") from exc ... Traceback (most recent call last): File "<stdin>", line 2, in <module> ZeroDivisionError: int division or modulo by zero The above exception was the direct cause of the following exception: Traceback (most recent call last): File "<stdin>", line 4, in <module> RuntimeError: Something bad happenedA similar mechanism works implicitly if an exception is raised inside an exception handler or a
finallyclause: the previous exception is then attached as the new exception’s__context__attribute:>>> try: ... print(1 / 0) ... except: ... raise RuntimeError("Something bad happened") ... Traceback (most recent call last): File "<stdin>", line 2, in <module> ZeroDivisionError: int division or modulo by zero During handling of the above exception, another exception occurred: Traceback (most recent call last): File "<stdin>", line 4, in <module> RuntimeError: Something bad happened
Also see the Built-in Exceptions documentation for details on the context and cause information attached to exceptions.
ANSWER 2
Score 20
In 2005, PEP 3134, Exception Chaining and Embedded Tracebacks introduced exception chaining:
- implicit chaining with explicit
raise EXCEPTIONor implicit raise (__context__attribute); - explicit chaining with explicit
raise EXCEPTION from CAUSE(__cause__attribute).
Motivation
During the handling of one exception (exception A), it is possible that another exception (exception B) may occur. In today’s Python (version 2.4), if this happens, exception B is propagated outward and exception A is lost. In order to debug the problem, it is useful to know about both exceptions. The
__context__attribute retains this information automatically.Sometimes it can be useful for an exception handler to intentionally re-raise an exception, either to provide extra information or to translate an exception to another type. The
__cause__attribute provides an explicit way to record the direct cause of an exception.[…]
Implicit Exception Chaining
Here is an example to illustrate the
__context__attribute:def compute(a, b): try: a/b except Exception, exc: log(exc) def log(exc): file = open('logfile.txt') # oops, forgot the 'w' print >>file, exc file.close()Calling
compute(0, 0)causes aZeroDivisionError. Thecompute()function catches this exception and callslog(exc), but thelog()function also raises an exception when it tries to write to a file that wasn’t opened for writing.In today’s Python, the caller of
compute()gets thrown anIOError. TheZeroDivisionErroris lost. With the proposed change, the instance ofIOErrorhas an additional__context__attribute that retains theZeroDivisionError.[…]
Explicit Exception Chaining
The
__cause__attribute on exception objects is always initialized toNone. It is set by a new form of theraisestatement:raise EXCEPTION from CAUSEwhich is equivalent to:
exc = EXCEPTION exc.__cause__ = CAUSE raise excIn the following example, a database provides implementations for a few different kinds of storage, with file storage as one kind. The database designer wants errors to propagate as
DatabaseErrorobjects so that the client doesn’t have to be aware of the storage-specific details, but doesn’t want to lose the underlying error information.class DatabaseError(Exception): pass class FileDatabase(Database): def __init__(self, filename): try: self.file = open(filename) except IOError, exc: raise DatabaseError('failed to open') from excIf the call to
open()raises an exception, the problem will be reported as aDatabaseError, with a__cause__attribute that reveals theIOErroras the original cause.Enhanced Reporting
The default exception handler will be modified to report chained exceptions. The chain of exceptions is traversed by following the
__cause__and__context__attributes, with__cause__taking priority. In keeping with the chronological order of tracebacks, the most recently raised exception is displayed last; that is, the display begins with the description of the innermost exception and backs up the chain to the outermost exception. The tracebacks are formatted as usual, with one of the lines:The above exception was the direct cause of the following exception:
or
During handling of the above exception, another exception occurred:
between tracebacks, depending whether they are linked by
__cause__or__context__respectively. Here is a sketch of the procedure:def print_chain(exc): if exc.__cause__: print_chain(exc.__cause__) print '\nThe above exception was the direct cause...' elif exc.__context__: print_chain(exc.__context__) print '\nDuring handling of the above exception, ...' print_exc(exc)[…]
In 2012, PEP 415, Implement Context Suppression with Exception Attributes introduced exception context suppression with explicit raise EXCEPTION from None (__suppress_context__ attribute).
Proposal
A new attribute on
BaseException,__suppress_context__, will be introduced. Whenever__cause__is set,__suppress_context__will be set toTrue. In particular,raise exc from causesyntax will setexc.__suppress_context__toTrue. Exception printing code will check for that attribute to determine whether context and cause will be printed.__cause__will return to its original purpose and values.There is precedence for
__suppress_context__with theprint_line_and_fileexception attribute.To summarize,
raise exc from causewill be equivalent to:exc.__cause__ = cause raise excwhere
exc.__cause__ = causeimplicitly setsexc.__suppress_context__.
So in PEP 415, the sketch of the procedure given in PEP 3134 for the default exception handler (its job is to report exceptions) becomes the following:
def print_chain(exc):
if exc.__cause__:
print_chain(exc.__cause__)
print '\nThe above exception was the direct cause...'
elif exc.__context__ and not exc.__suppress_context__:
print_chain(exc.__context__)
print '\nDuring handling of the above exception, ...'
print_exc(exc)
ANSWER 3
Score 5
The shortest answer. PEP-3134 says it all. raise Exception from e sets the __cause__ filed of the new exception.
A longer answer from the same PEP:
__context__field would be set implicitly to the original error insideexcept:block unless told not to with__suppress_context__ = True.__cause__is just like context but has to be set explicitly by usingfromsyntaxtracebackwill always chain when you callraiseinside anexceptblock. You can get rid of traceback by a) swallowing an exceptionexcept: passor by messing withsys.exc_info()directly.
The long answer
import traceback
import sys
class CustomError(Exception):
def __init__(self):
super().__init__("custom")
def print_exception(func):
print(f"\n\n\nEXECURTING FUNCTION '{func.__name__}' \n")
try:
func()
except Exception as e:
"Here is result of our actions:"
print(f"\tException type: '{type(e)}'")
print(f"\tException message: '{e}'")
print(f"\tException context: '{e.__context__}'")
print(f"\tContext type: '{type(e.__context__)}'")
print(f"\tException cause: '{e.__cause__}'")
print(f"\tCause type: '{type(e.__cause__)}'")
print("\nTRACEBACKSTART>>>")
traceback.print_exc()
print("<<<TRACEBACKEND")
def original_error_emitter():
x = {}
print(x.does_not_exist)
def vanilla_catch_swallow():
"""Nothing is expected to happen"""
try:
original_error_emitter()
except Exception as e:
pass
def vanilla_catch_reraise():
"""Nothing is expected to happen"""
try:
original_error_emitter()
except Exception as e:
raise e
def catch_replace():
"""Nothing is expected to happen"""
try:
original_error_emitter()
except Exception as e:
raise CustomError()
def catch_replace_with_from():
"""Nothing is expected to happen"""
try:
original_error_emitter()
except Exception as e:
raise CustomError() from e
def catch_reset_trace():
saw_an_error = False
try:
original_error_emitter()
except Exception as e:
saw_an_error = True
if saw_an_error:
raise CustomError()
print("Note: This will print nothing")
print_exception(vanilla_catch_swallow)
print("Note: This will print AttributeError and 1 stack trace")
print_exception(vanilla_catch_reraise)
print("Note: This will print CustomError with no context but 2 stack traces")
print_exception(catch_replace)
print("Note: This will print CustomError with AttributeError context and 2 stack traces")
print_exception(catch_replace_with_from)
print("Note: This will brake traceback chain")
print_exception(catch_reset_trace)
Will result in the following output:
Note: This will print nothing
EXECURTING FUNCTION 'vanilla_catch_swallow'
Note: This will print AttributeError and 1 stack trace
EXECURTING FUNCTION 'vanilla_catch_reraise'
Exception type: '<class 'AttributeError'>'
Exception message: ''dict' object has no attribute 'does_not_exist''
Exception context: 'None'
Context type: '<class 'NoneType'>'
Exception cause: 'None'
Cause type: '<class 'NoneType'>'
TRACEBACKSTART>>>
Traceback (most recent call last):
File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 11, in print_exception
func()
File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 41, in vanilla_catch_reraise
raise e
File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 39, in vanilla_catch_reraise
original_error_emitter()
File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 27, in original_error_emitter
print(x.does_not_exist)
AttributeError: 'dict' object has no attribute 'does_not_exist'
<<<TRACEBACKEND
Note: This will print CustomError with no context but 2 stack traces
EXECURTING FUNCTION 'catch_replace'
Exception type: '<class '__main__.CustomError'>'
Exception message: 'custom'
Exception context: ''dict' object has no attribute 'does_not_exist''
Context type: '<class 'AttributeError'>'
Exception cause: 'None'
Cause type: '<class 'NoneType'>'
TRACEBACKSTART>>>
Traceback (most recent call last):
File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 46, in catch_replace
original_error_emitter()
File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 27, in original_error_emitter
print(x.does_not_exist)
AttributeError: 'dict' object has no attribute 'does_not_exist'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 11, in print_exception
func()
File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 48, in catch_replace
raise CustomError()
CustomError: custom
<<<TRACEBACKEND
Note: This will print CustomError with AttributeError context and 2 stack traces
EXECURTING FUNCTION 'catch_replace_with_from'
Exception type: '<class '__main__.CustomError'>'
Exception message: 'custom'
Exception context: ''dict' object has no attribute 'does_not_exist''
Context type: '<class 'AttributeError'>'
Exception cause: ''dict' object has no attribute 'does_not_exist''
Cause type: '<class 'AttributeError'>'
TRACEBACKSTART>>>
Traceback (most recent call last):
File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 53, in catch_replace_with_from
original_error_emitter()
File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 27, in original_error_emitter
print(x.does_not_exist)
AttributeError: 'dict' object has no attribute 'does_not_exist'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 11, in print_exception
func()
File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 55, in catch_replace_with_from
raise CustomError() from e
CustomError: custom
<<<TRACEBACKEND
Note: This will brake traceback chain
EXECURTING FUNCTION 'catch_reset_trace'
Exception type: '<class '__main__.CustomError'>'
Exception message: 'custom'
Exception context: 'None'
Context type: '<class 'NoneType'>'
Exception cause: 'None'
Cause type: '<class 'NoneType'>'
TRACEBACKSTART>>>
Traceback (most recent call last):
File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 11, in print_exception
func()
File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 64, in catch_reset_trace
raise CustomError()
CustomError: custom
<<<TRACEBACKEND
ANSWER 4
Score 0
Preamble
This is technically explained in the documentation. But it's very confusing and lacks examples. Here's some examples that I think explain the situation much more clearly than the __context__, __cause__, __supress_context__ funny business.
The main examples
import sys
from traceback import print_exc
class ExceptionA(Exception): pass
class ExceptionB(Exception): pass
print('## Naked exception ##')
try:
raise ExceptionA
except:
print_exc(file=sys.stdout)
print('\n\n\n## except and raise ##')
try:
try:
raise ExceptionA
except ExceptionA:
raise ExceptionB
except:
print_exc(file=sys.stdout)
print('\n\n\n## except and raise from e ##')
try:
try:
raise ExceptionA
except ExceptionA as e:
raise ExceptionB from e
except:
print_exc(file=sys.stdout)
print('\n\n\n## except and raise from None ##')
try:
try:
raise ExceptionA
except ExceptionA:
raise ExceptionB from None
except:
print_exc(file=sys.stdout)
yields
## Naked exception ##
Traceback (most recent call last):
File "...\raise.py", line 9, in <module>
raise ExceptionA
ExceptionA
## except and raise ##
Traceback (most recent call last):
File "...\raise.py", line 16, in <module>
raise ExceptionA
ExceptionA
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "...\raise.py", line 18, in <module>
raise ExceptionB
ExceptionB
## except and raise from e ##
Traceback (most recent call last):
File "...\raise.py", line 25, in <module>
raise ExceptionA
ExceptionA
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "...\raise.py", line 27, in <module>
raise ExceptionB from e
ExceptionB
## except and raise from None ##
Traceback (most recent call last):
File "...\raise.py", line 36, in <module>
raise ExceptionB from None
ExceptionB
So we get 4 behaviors.
- With no exception "handling" we just print out the
ExceptionA. - When a new exception B is directly raised during the handling of exception A we see tracebacks for A and B concatenated by the message "During handling of the above exception, another exception occurred:"
- When a new exception B is raised from A during the handling of A we see tracebacks for A and B concatenated by the message "The above exception was the direct cause of the following exception:"
- When a new exception B is raised from None during the handling of A wee only see the traceback for B.
Why raise from e?
I struggle to see the practical difference between options 2 and 3. The only difference is the message between the concatenated tracebacks, so the difference purely has to do with human readers. So the two have different semantics. From what I can gather, the semantics of the second option (raise ExceptionB) say: We were handling exception A and then another unexpected exception B happened. The third option (raise ExceptionB from e) says that we are sort of transforming ExceptionB from an internal exception into a possibly more informative user-facing exception. I think a common use case would be something like
from traceback import print_exc
import sys
my_dict = {'a': 1}
print('## No handling ##')
try:
print(my_dict['b'])
except:
print_exc(file=sys.stdout)
print('\n\n\n## Handling the exception to add more info for the reader ##')
try:
try:
print(my_dict['b'])
except KeyError as e:
raise KeyError('You forgot to include a key in your dict!') from e
except:
print_exc(file=sys.stdout)
yields
## No handling ##
Traceback (most recent call last):
File "...\raise_2.py", line 9, in <module>
print(my_dict['b'])
KeyError: 'b'
## Handling the exception to add more info for the reader ##
Traceback (most recent call last):
File "...\raise_2.py", line 16, in <module>
print(my_dict['b'])
KeyError: 'b'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "...\raise_2.py", line 18, in <module>
raise KeyError('You forgot to include a key in your dict!') from e
KeyError: 'You forgot to include a key in your dict!'
The point here is we don't want the traceback to just point out that the second key error "occurred during the handling of the first key error". It's more helpful to point out that the second key error occurred BECAUSE the first key error occurred. That is, the have the same cause (hence __cause__). So in this sense the developer can communicate a little bit more information to whoever is reading the traceback. However, in some cases you may want to suppress the underlying cause entirely and provide the user a shorter more descriptive traceback. In this case you use raise ... from None.