Stefan Scherfke

Raise … from … in Python

When you recently upgraded to pylint 2.6.0, you may have stumbled across a new warning:

src/mylib/core.py:74:20: W0707:
  Consider explicitly re-raising using the
  'from' keyword (raise-missing-from)

The reason for this message is an exception that you raised from within an except block like this:

>>> class MyLibError(Exception):
...     """Base class for all errors raised by mylib"""
...
>>> def do_stuff("onoes"):
...     try:
...         int(text)
...     except ValueError as e:
...         raise MyLibError(e)

When you run do_stuff(), you’ll get the following traceback:

>>> do_stuff(text)
Traceback (most recent call last):
  File "<stdin>", line 3, in do_stuff
ValueError: invalid literal for int() with base 10: 'onoes'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in do_stuff
__main__.MyLibError: invalid literal for int() with base 10: 'onoes'

The important line here is:

During handling of the above exception, another exception occurred

This means that while you were handling the ValueError, another (unexpected) exception occurred: a MyLibError.

But this is not what we wanted to do – we wanted to replace the ValueError with MyLibError, so that our uses only have to handle a single exception type!

Enter raise … from …

To express I want to modify and forward an existing exception, you can use the raise NewException from cause syntax:

>>> def do_stuff(text):
...     try:
...         int(text)
...     except ValueError as e:
...         raise MyLibError(e) from e

When we run this now, we’ll get a different traceback printed:

>>> do_stuff("onoes")
Traceback (most recent call last):
  File "<stdin>", line 3, in do_stuff
ValueError: invalid literal for int() with base 10: 'onoes'

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

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in do_stuff
__main__.MyLibError: invalid literal for int() with base 10: 'onoes'

Your users will now receive a MyLibError with the attached information, that the cause of this error was a ValueError somewhere in your code.

When the underlying cause is not important

If your users shouldn’t care about the underlying cause, because the new exception contains all the relevant information (i.e., that the provided input cannot be parsed), you may also omit the cause:

>>> def do_stuff(text):
...     try:
...         int(text)
...     except ValueError as e:
...         raise MyLibError(e) from None

When you run this now, you’ll get a nice an clean traceback:

>>> do_stuff("onoes")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in do_stuff
__main__.MyLibError: invalid literal for int() with base 10: 'onoes'

Summary

When you raise an exception from within an except block in Python, you have three options:

  • If a new/unexpected exception occurs in the code handling the original exception, raise NewException.
  • If you want to wrap the original exception(s) (e.g., with a common base exception to reduce complexity for your users), raise NewException from cause.
  • If you want to hide the original exception because it is irrelevant for your users, raise NewException from None.