Python "raise from" usage
Asked Answered
E

3

446

What's the difference between raise and raise from in Python?

try:
    raise ValueError
except Exception as e:
    raise IndexError

which yields

Traceback (most recent call last):
  File "tmp.py", line 2, in <module>
    raise ValueError
ValueError

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "tmp.py", line 4, in <module>
    raise IndexError
IndexError

and

try:
    raise ValueError
except Exception as e:
    raise IndexError from e

which yields

Traceback (most recent call last):
  File "tmp.py", line 2, in <module>
    raise ValueError
ValueError

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

Traceback (most recent call last):
  File "tmp.py", line 4, in <module>
    raise IndexError from e
IndexError
Erose answered 15/7, 2014 at 7:33 Comment(6)
Have you read PEP-3134?Derbyshire
Now use raise IndexError from None, say.Boilermaker
Heh. raise IndexError from False raises a TypeError, not an IndexError. Made my day.Huddleston
Related: What is the difference between __cause__ and __context__?Gehlenite
Not sure if this is the right place to mention it, but to anyone using Spyder: This entire construct does not work there. It's been an issue for over 3 years now (github.com/spyder-ide/spyder/issues/2943), but they seem to think there is no need for chained exceptions.Ovipositor
A working link to pep-3134Deirdredeism
B
494

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 from clause 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 happened

A similar mechanism works implicitly if an exception is raised inside an exception handler or a finally clause: 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.

Boilermaker answered 15/7, 2014 at 7:44 Comment(19)
Is there any reason to explicitly chain the exceptions using from and __cause__ in lieu of the implicit __context__? Are there any cases where one would attach a different exception than the one caught by the except?Erose
@darkfeline: Lets say your database API supports opening databases from various sources, including the web and disk. Your API will always raise a DatabaseError if opening the database fails. But if the failure is the result of a IOError because a file failed to open or a HTTPError because a URL failed to work then that is context you want to include explicitly, so the developer using the API can debug why this is. At that moment you use raise DatabaseError from original_exception.Boilermaker
@darkfeline: If that developer is wrapping the use of the database API in their own API and wanted to pass on that IOError or HTTPError on to their consumers, then they'd have to use raise NewException from databaseexception.__cause__, now using a different exception from the DatabaseException that they just caught.Boilermaker
__suppress_context__ = True in your answer should be just __suppress_context__, no? Because the later part suggests it's only set to True if raise ... from None is used.Byzantine
@Byzantine not sure what you mean; you can set it explicitly to True, but it is automatically set to True for you when you use any raise ... from ... (None or otherwise).Boilermaker
After reading again I understand now. I assumed that __suppress_context__ would only be set to true if it was raised from None but it the traceback is printed differently if __cause__ is set. Otherwise both __cause__ and __context__ would be printed.Byzantine
Is there any from __future__ for the raise .. from construct in Python 2?Wormy
@dan3: no, there isn't. Exception chaining is purely a Python 3 feature.Boilermaker
So in short, use raise from whenever I want to keep the original exception object, correct?Housewife
@laike9m: you mean when you are handling exception foo, and want to raise a new exception bar? Then you can use raise bar from foo and have Python state that foo directly caused bar. If you don't use from foo, then Python will still print both, but state that during handling foo, bar was raised, Different message, intended to flag a possible bug in the error handling.Boilermaker
I mean the newly raised exception's __cause__ attr is the original exception, which maybe useful for future error handling, while not using raise from throws the original exception object away.Housewife
@laike9m: no, not when you used raise from inside an exception handler. __context__ is still set then.Boilermaker
So it only depends on what you need the printed message to be?Housewife
@laike9m: exactly. Both are meant to be debugging aids, not end-user UI of course.Boilermaker
If code has been written for Python 3 and you want to modify it to work both on Python 2 and 3, what's the best way of doing that? Should I replace raise Error(...) from e with e2 = Error(...); e2.__cause__ = e; raise e2 (except on separate lines, of course.) That would be the same as before on Python 3, and work on Python 2.7, right? Otherwise I'd need to have separate files for Python 2 and 3 and be careful when importing to check the version...Biogenesis
@ArtOfWarfare: yes, you'd have to create the instance, set the __cause__ attribute manually, then raise the updated exception. This works in 2 and 3. However, in Python 3, you may end up with a circular reference if you then don't delete the e2 variable (as the __traceback__ attribute that raise sets will contain an indirect reference to the frame and thus to the e2 name, etc.). The six project creates a function that uses try...finally.Boilermaker
@MartijnPieters - One of us have misread the code in the six project. Line 731 is an else, for if your Python version is below 3.2. Line 733 shows that it'll simply raise value and ignore the from_value argument.Biogenesis
@MartijnPieters - Maybe I should finally just move on from Python 2.7... the final library I needed that I was waiting on (Fabric) appears to have a well maintained port now available in Python 3. I'll only be 8 years late to the party, and I always made sure my code ran in both versions... now I can finally abandon 2.7?Biogenesis
@ArtOfWarfare: If you are writing polyglot code, then it'll be run on both 2.7 and on 3.x, so in the 3.x case you really want the finally part to run; you did just set __cause__ after all.Boilermaker
N
20

In 2005, PEP 3134, Exception Chaining and Embedded Tracebacks introduced exception chaining:

  • implicit chaining with explicit raise EXCEPTION or 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 a ZeroDivisionError. The compute() function catches this exception and calls log(exc), but the log() 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 an IOError. The ZeroDivisionError is lost. With the proposed change, the instance of IOError has an additional __context__ attribute that retains the ZeroDivisionError.

[…]

Explicit Exception Chaining

The __cause__ attribute on exception objects is always initialized to None. It is set by a new form of the raise statement:

raise EXCEPTION from CAUSE

which is equivalent to:

exc = EXCEPTION
exc.__cause__ = CAUSE
raise exc

In 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 DatabaseError objects 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 exc

If the call to open() raises an exception, the problem will be reported as a DatabaseError, with a __cause__ attribute that reveals the IOError as 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 to True. In particular, raise exc from cause syntax will set exc.__suppress_context__ to True. 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 the print_line_and_file exception attribute.

To summarize, raise exc from cause will be equivalent to:

exc.__cause__ = cause
raise exc

where exc.__cause__ = cause implicitly sets exc.__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)
Nicholle answered 17/3, 2022 at 15:21 Comment(0)
H
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 inside except: block unless told not to with __suppress_context__ = True.
  • __cause__ is just like context but has to be set explicitly by using from syntax
  • traceback will always chain when you call raise inside an except block. You can get rid of traceback by a) swallowing an exception except: pass or by messing with sys.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
Hyades answered 7/12, 2022 at 17:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.