Exceptions¶
Very early Python versions used simple strings to signalize errors. Later, Python allowed raising arbitrary classes, and added specialized exception classes to the standard library. For backwards compatibility reasons, some deprecated practices were still allowed in Python 2. This presents confusion to learners of the language, and prevents some performance optimizations.
Python 3 removes the deprecated practices. It also further consolidates the exception model. Exceptions are now instances of dedicated classes, and contain all information about the error: the type, value and traceback.
This chapter mentions all exception-related changes needed to start supporting Python 3.
The new except
syntax¶
- Fixer:
python-modernize -wnf fissix.fixes.fix_except
- Prevalence: Very common
In Python 2, the syntax for catching exceptions was
except ExceptionType:
, or except ExceptionType, target:
when the
exception object is desired.
ExceptionType
can be a tuple, as in, for example,
except (TypeError, ValueError):
.
This could result in hard-to-spot bugs: the command
except TypeError, ValueError:
(note lack of parentheses) will only handle
TypeError
. It will also assign the exception object to the name
ValueError
, shadowing the built-in.
To fix this, Python 2.6 introduced an alternate syntax:
except ExceptionType as target:
.
In Python 3, the old syntax is no longer allowed.
You will need to switch to the new syntax. The recommended fixer works quite reliably, and it also fixes the Iterating Exceptions problem described below.
The new raise
syntax¶
- Fixer:
python-modernize -wnf libmodernize.fixes.fix_raise -f libmodernize.fixes.fix_raise_six
- Prevalence: Common
Python 2’s raise
statement was designed at a time when exceptions weren’t
classes, and an exception’s type, value, and traceback components
were three separate objects:
raise ValueError, 'invalid input'
raise ValueError, 'invalid input', some_traceback
In Python 3, one single object includes all information about an exception:
raise ValueError('invalid input')
e = ValueError('invalid input')
e.__traceback__ = some_traceback
raise e
Python 2.6 allows the first variant. For the second, re-raising an exception, the Compatibility library: six library includes a convenience wrapper that works in both versions:
import six
six.reraise(ValueError, 'invalid input', some_traceback)
The recommended fixers will do these conversions automatically and quite reliably, but do verify the resulting changes.
Caught Exception “Scope”¶
- Fixer: None
- Prevalence: Rare
As discussed previously, in Python 3, all information about an exception, including the traceback, is contained in the exception object. Since the traceback holds references to the values of all local variables, storing an exception in a local variable usually forms a reference cycle, keeping all local variables allocated until the next garbage collection pass.
To prevent this issue, to quote from Python’s documentation:
When an exception has been assigned using as target, it is cleared at the end of the except clause. This is as if
except E as N: foowas translated to
except E as N: try: foo finally: del NThis means the exception must be assigned to a different name to be able to refer to it after the except clause.
Unfortunately, Automated fixer: python-modernize does not provide a fixer for this
change.
This issue results in a loud NameError
when tests are run.
When you see this error, apply the recommended fix – assign a different name
to the exception to use it outside the except
clause.
Iterating Exceptions¶
- Fixer:
python-modernize -wnf fissix.fixes.fix_except
(but see caveat below) - Prevalence: Rare
In Python 2, exceptions were iterable, so it was possible to “unpack” the
arguments of an exception as part of the except
statement:
except RuntimeError as (num, message):
In Python 3, this is no longer true, and the arguments must be accessed through
the args
attribute:
except RuntimeError as e:
num, message = e.args
The recommended fixer catches the easy cases of unpacking in except
statements.
If your code iterates through exceptions elsewhere, you need to manually
change it to iterate over args
instead.
Additionally, the fixer does not do a good job on single-line suites such as:
except RuntimeError as (num, message): pass
Inspect the output and break these into multiple lines manually.
Raising Non-Exceptions¶
- Fixer: None
- Prevalence: Rare
In Python 3, an object used with raise
must be an instance of
BaseException
, while Python 2 also allowed old-style classes.
Similarly, Python 3 bans catching non-exception classes in the except
statement.
Raising non-Exception classes was obsolete as early as in Python 2.0, but code that does this can still be found.
Each case needs to be handled manually.
If there is a dedicated class for the exception,
make it inherit from Exception
.
Otherwise, switch to using a dedicated Exception class.
The Removed StandardError
¶
- Fixer:
python-modernize -wnf fissix.fixes.fix_standarderror
(but see caveat below) - Prevalence: Rare
The StandardError
class is removed in Python 3.
It was the base class for built-in exceptions, and it proved to be an
unnecessary link in almost any exception’s inheritance chain.
The recommended fixer will replace all uses of StandardError
with
Exception
.
Review the result to check if this is correct.
Some code might rely on the name of an exception class, or on exceptions not
derived from StandardError
, or otherwise handle StandardError
specially. You’ll need to handle these casses manually.
Removed sys.exc_type
, sys.exc_value
, sys.exc_traceback
¶
- Fixer: None
- Prevalence: Rare
These exception-related attributes of the sys
module are not thread-safe,
and were deprecated since Python 1.5.
They have been dropped for Python 3.
The information can be retrieved with a call to exc_info()
:
exc_type, exc_value, exc_traceback = sys.exc_info()