100

Is there a standard way of using exception chains in Python? Like the Java exception 'caused by'?

Here is some background.

I have a module with one main exception class DSError:

 class DSError(Exception):
     pass

Somewhere within this module there will be:

try:
    v = my_dict[k]
    something(v)
except KeyError as e:
    raise DSError("no key %s found for %s" % (k, self))
except ValueError as e:
    raise DSError("Bad Value %s found for %s" % (v, self))
except DSError as e:
    raise DSError("%s raised in %s" % (e, self))

Basically this snippet should throw only DSError and tell me what happened and why. The thing is that the try block might throw lots of other exceptions, so I'd prefer if I can do something like:

try:
    v = my_dict[k]
    something(v)
except Exception as e:
    raise DSError(self, v, e)  # Exception chained...

Is this standard pythonic way? I did not see exception chains in other modules so how is that done in Python?

jamylak
  • 128,818
  • 30
  • 231
  • 230
Ayman
  • 11,265
  • 16
  • 66
  • 92
  • What do you want the output to be? I can't tell if you actually want the original exception's stack trace, or if you want to hide it and just have your own exception with a single message that summarizes the original exception? – BrenBarn May 07 '13 at 08:49
  • The original trace would be much better, since the try block may be called recursively from the module. – Ayman May 07 '13 at 08:56

2 Answers2

158

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)
Mr_and_Mrs_D
  • 32,208
  • 39
  • 178
  • 361
phihag
  • 278,196
  • 72
  • 453
  • 469
  • 2
    For python 2 if one wants to save the traceback - which one _must_ want - [`raise MyError(message + u', caused by ' + repr(cause)), None, sys.exc_info()[2]`](http://stackoverflow.com/a/1350981/281545) – Mr_and_Mrs_D May 29 '15 at 23:39
  • 3
    "In most cases, you don't even need the from" Do you have an example where it is needed or useful? – timgeb Jan 12 '16 at 22:46
  • 3
    @timgeb [PEP 3134](https://www.python.org/dev/peps/pep-3134/) has two situations for chaining: one where error handling code results in another exception being raised, and the other where an exception was deliberately translated to a different exception. The `from e` is for the deliberate case, and changes the message in the output as shown in the answer above. – Eric Smith Jan 19 '17 at 17:14
  • 2
    This response is, in my opinion, better than the duplicated question's selected answer, as this covers Python 3. Also, kudos for noting that the `from` isn't even necessary, as Python 3 tracebacks already print all current exceptions in the stack. – Alvaro Gutierrez Perez Oct 31 '17 at 02:21
  • upvoted! what would you do if you also want to return a value from this – PirateApp Jul 14 '18 at 13:24
  • @PirateApp You can either raise or return, can't do both. But if you need a value, you can simply attach it to the exception object, for instance by making the value a property of your custom error class (`MyError` here). – phihag Jul 14 '18 at 13:39
  • So in python 3 chaining is default behaviour? And only when deliberately raising another exception of we want to get full trace then we use the raise from keyword? So are raise ex, raise, raise from all the same? – variable Jul 24 '19 at 04:58
  • @timgeb, I'm late to the party but found a [blog post](https://blog.ram.rachum.com/post/621791438475296768/improving-python-exception-chaining-with) which illustrates a case which demonstrates the need and usefulness of exception chaining. – Michael Ruth Oct 11 '22 at 23:20
5

Is this what you're asking for?

class MyError(Exception):
    def __init__(self, other):
        super(MyError, self).__init__(other.message)

>>> try:
...     1/0
... except Exception, e:
...     raise MyError(e)
Traceback (most recent call last):
  File "<pyshell#27>", line 4, in <module>
    raise MyError(e)
MyError: division by zero

If you want to store the original exception object, you can certainly do so in your own exception class's __init__. You might actually want to store the traceback as the exception object itself doesn't provide much useful information about where the exception occurred:

class MyError(Exception):
    def __init__(self, other):
        self.traceback = sys.exc_info()
        super(MyError, self).__init__(other.message)

After this you can access the traceback attribute of your exception to get info about the original exception. (Python 3 already provides this as the __traceback__ attribute of an exception object.)

BrenBarn
  • 242,874
  • 37
  • 412
  • 384
  • Almost right, but I considered this would be 'cheating', since it only takes the message of the chained exception, not the actual exception object. I.e. I would not know where the actual division by zero occurred, just that it was caught _somewhere_. – Ayman May 07 '13 at 08:53
  • @Ayman: See my edited answer. All you have to do is grab the traceback and store it. However, if you really want all the information from the original exception to show up in the traceback like a real exception, then phihag is right that this can't be accomplished in Python 2. You'd have to just manually print the old traceback as part of your exception's message. – BrenBarn May 07 '13 at 09:00
  • Thanks. I didn't know about the sys.exc_info(). I would accept this as the answer too :-) – Ayman May 07 '13 at 09:07
  • 1
    is `other.message` always present ? – Mr_and_Mrs_D Jan 21 '15 at 19:10