On 2/12/2013 1:15 AM, Steven D'Aprano wrote:
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.

You do not need Rick to justify such an informative post.

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__.

Some thought was given to having only one special attribute, but in the end it was decided to have __context__ be the actual context and __cause__ be the programmer set and displayed '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

The new features are explained in the Library manual, Ch. 5, Exceptions, but without so many clear examples. The 'from None' option has not yet been added to the Language reference section on raise statements (an issue on the tracker), so it is easy to miss if one does not also read the Library chapter.

You can read more about exception chaining here:

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

--
Terry Jan Reedy

--
http://mail.python.org/mailman/listinfo/python-list

Reply via email to