Awsome Python - chained exceptions

Steven D'Aprano steve+comp.lang.python at pearwood.info
Tue Feb 12 07:15:29 CET 2013


As an antidote to the ill-informed negativity of Ranting Rick's 
illusionary "PyWarts", I thought I'd present a few of Python's more 
awesome features, starting with exception contexts.

If you've ever written an exception handler, you've probably written a 
*buggy* exception handler:


def getitem(items, index):
    # One-based indexing.
    try:
        return items[index-1]
    except IndexError:
        print ("Item at index %d is missing" % index - 1)  # Oops!


Unfortunately, when an exception occurs inside an except or finally 
block, the second exception masks the first, and the reason for the 
original exception is lost:

py> getitem(['one', 'two', 'three'], 5)  # Python 2.6
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in getitem
TypeError: unsupported operand type(s) for -: 'str' and 'int'


But never fear! In Python 3.1 and better, Python now shows you the full 
chain of multiple exceptions, and exceptions grow two new special 
attributes: __cause__ and __context__.

If an exception occurs while handling another exception, Python sets the 
exception's __context__ and displays an extended error message:


py> getitem(['one', 'two', 'three'], 5)  # Python 3.1
Traceback (most recent call last):
  File "<stdin>", line 4, in getitem
IndexError: list index out of range

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in getitem
TypeError: unsupported operand type(s) for -: 'str' and 'int'



Python 3 also allows you to explicitly set the exception's __cause__ 
using "raise...from" syntax:

py> try:
...     len(None)
... except TypeError as e:
...     raise ValueError('bad value') from e
... 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
TypeError: object of type 'NoneType' has no len()

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

Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
ValueError: bad value



Note the slight difference in error message. If both __cause__ and 
__context__ are set, the __cause__ takes priority.


Sometimes you actually want to deliberately catch one exception and raise 
another, without showing the first exception. A very common idiom in 
Python 2:


try:
    do_work()
except SomeInternalError:
    raise PublicError(error_message)


Starting with Python 3.3, there is now support from intentionally 
suppressing the __context__:


py> try:
...     len(None)
... except TypeError:
...     raise ValueError('bad value') from None  # Python 3.3
... 
Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
ValueError: bad value



You can read more about exception chaining here:

http://www.python.org/dev/peps/pep-3134/
http://www.python.org/dev/peps/pep-0409/



-- 
Steven



More information about the Python-list mailing list