WEBINAR:
On-Demand
Application Security Testing: An Integral Part of DevOps
PEP-3109 Raising Exceptions in Python 3000
In Python 2.X you can raise exceptions in two equivalent ways:
raise E, V
raise E(V)
In both cases E is the exception class and V is the value. However, Python 3.0 allows only the second form. Python 2.X also supports one other (rare) way to raise an exception with an arbitrary traceback:
raise E, V, T
In Python 3.0 that becomes:
e = E(V)
e.__traceback__ = T
raise E
Although the Python 3.0 form is more cumbersome, because this form of raising exceptions is so rare (I can't think of a single use where I would want to attach an arbitrary traceback to an exception) that may not be important. If you want to re-raise the active exception inside an
except or
finally block, just continue using
raise with no arguments.
Generators had an asynchronous facility for raising exceptions via the
throw() method that accepted three arguments similar to the
raise E, V, T form. In Python 3.0
throw() accepts only a single exception argument, so it's consistent with raise.
PEP-3110 Catching Exceptions in Python 3000
There are a few problems with the syntax and semantics of exception catching in Python 2.x. For example, to catch an exception type and its instance you use the following semantics:
except <expression_1>, <expression_2>:
If you needed to catch just a couple of exception types, you could wrap the types in parentheses:
except (<expression_1>, <expression_2>):
Unfortunately, that syntax can lead to some pretty nasty bugs if you forget the parentheses. In the following code snippet the
except clause catches Exception and
str (without parentheses). The intention is to catch either the general Exception class or the
str type. The result is that only the Exception type is caught. The specific instance of Exception (DivisionInZeroException in this case) gets assigned to
str. Trying to use
str() later ends in an obscure and catastrophic fashion.
>>> try:
... 3 / 0
... except Exception, str:
... print repr(str)
...
ZeroDivisionError('integer division or modulo by zero',)
>>> str(5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'exceptions.ZeroDivisionError' object is not callable
If this exception-handling code happened to buried somewhere deep in your code, so that
str() was used much later, it could be pretty difficult to locate.
Python 3.0 addresses these issues by replacing the first form (
except TYPE, INSTANCE) with the following form:
except <exception type> as <instance>
In addition, the exception type
must derive from
exceptions.BaseException. If you want to catch multiple exception types you may still use the tuple syntax. Here's an example of how to catch exceptions in Python 3.0:
>>> try:
... 3 / 0
... except Exception as e:
... print(repr(e))
...
ZeroDivisionError('int division or modulo by zero',)
>>> try:
... 3 / 0
... except (Exception, ZeroDivisionError):
... pass
...
Other changes related to exception catching are that the
sys.exc_type,
sys.exc_value, and
sys.exc_traceback have been removed. The information is available as the tuple returned from
sys.exc_info():
>>> try:
... 3/0
... except:
... print(sys.exc_info())
...
(<class 'ZeroDivisionError'>, ZeroDivisionError('int division or modulo by zero',),
<traceback object at 0x3bcbc0>)
Even
sys.exc_info() may go away in the future. Alternatives like exception attributes were discussed—but dropped due to the difficulties involved in auto-fixing existing code. The
traceback has become an attribute as you will see in the next section.
Finally, Python 3.0 will make sure that the exception instance gets deleted at the end of the
except clause to avoid garbage collection issues.
PEP-3134 Exception Chaining and Embedded Tracebacks
Exception objects in Python 3.0 acquire three new attributes:
__context__,
__cause__, and
__traceback__. The
__context__ attribute is useful when you explicitly raise an exception that was originally raised during the handling of another exception. In Python 2.x the original exception was lost and replaced by the second exception as shown below:
>>> try:
... raise Exception('first')
... except:
... raise Exception('second')
...
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
Exception: second
Here's the same code running in Python 3.0:
>>> try:
... raise Exception('first')
... except:
... raise Exception('second')
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
Exception: first
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
Exception: second
As you can see the second exception object stores the original exception object in its
__context__ attribute:
>>> e.__context__
Exception('first',)
>>> type(e.__context__)
<class 'Exception'>
This process of exception chaining can go on and on if you have nested
try-except blocks. Each exception stores the preceding exception in its
__context__.
The
__cause__ attribute is useful when you want to explicitly translate one exception to another. The syntax is:
raise EXCEPTION from CAUSE.
CAUSE must be an exception too. Here's an example:
try:
raise Exception('duh!') from Exception('Gigi')
except Exception as e:
assert e.__context__ is None
assert repr(e.__cause__) == "Exception('Gigi',)"
I don't really understand why this syntax is needed. The
from CAUSE is just syntactic sugar, because (except that
CAUSE will be stored in
__context__) it's equivalent to:
try:
try:
raise CAUSE
except:
raise EXCEPTION
The fact that in one form the original exception is stored in
__context__ and in the other form it's stored in
__cause__ might lead to confusing and complex error handling code where you have to check not only the caught exception, but also the
__cause__ and the
__context__ because you can't be sure what to expect. I can accept the syntactic sugar of
from CAUSE, but I think
CAUSE should just be stored in
__context__.
The last new Exception attribute is
__traceback__, which is a traceback object that tells you the exception origin. In Python 2.x you had to
call sys.exc_info()[2] or
sys.exc_traceback to get the traceback object. Now, it's an attribute of the exception object itself, which makes it more discoverable and the code more concise. Remember to use the traceback module to make sense of traceback objects:
import traceback
def f():
g()
def g():
raise Exception('nested')
t = None
try:
f()
except Exception as e:
t = e.__traceback__
print('----------------')
print('The stack trace')
print('----------------')
traceback.print_tb(t)
print('----------------')
----------------
The stack trace
----------------
File "article_1.py", line 244, in <module>
f()
File "article_1.py", line 237, in f
g()
File "article_1.py", line 240, in g
raise Exception('nested')
----------------
This first article in the "Developer's Guide to Python 3.0" series provided information about the important changes to the core language and its type system. The next article in the series focuses on how Python 3.0 treats the basic data types: numbers, text and binary data.