Книга: Practical Programming, Fourth Edition
Назад: Common Built-in Exceptions
Дальше: Defining Custom Exceptions

Raising and Chaining Exceptions

Raising an exception in Python is as simple as using the raise keyword. The raise statement takes an exception object, either built-in or user-defined (and, in exceptional situations, objects of other types), and immediately stops normal execution at that point.

​ ​>>>​​ ​​raise​​ ​​ZeroDivisionError​
​ Traceback (most recent call last):
​  File "<python-input-0>", line 1, in <module>
​  raise ZeroDivisionError
​ ZeroDivisionError
​ ​>>>​​ ​​raise​​ ​​Exception​
​ Traceback (most recent call last):
​  File "<python-input-1>", line 1, in <module>
​  raise Exception
​ Exception
​ ​>>>​​ ​​raise​​ ​​Exception()​
​ Traceback (most recent call last):
​  File "<python-input-2>", line 1, in <module>
​  raise Exception()
​ Exception

You should raise exceptions in a function as soon as an error becomes detectable. For example, the function below multiplies a list by a given number of copies. Before doing so, it checks whether the number is positive. If it isn’t, the function raises a ValueError.

Without this check, the function would silently return an empty list, which is likely not what the caller intended:

​ ​>>>​​ ​​def​​ ​​multiply_list(l:​​ ​​list,​​ ​​n:​​ ​​int)​​ ​​->​​ ​​list:​
​ ​...​​ ​​"""Return list `l` replicated `n` times."""​
​ ​...​​ ​​if​​ ​​n​​ ​​<=​​ ​​0:​
​ ​...​​ ​​raise​​ ​​ValueError​
​ ​...​​ ​​return​​ ​​n​​ ​​*​​ ​​l​
​ ​...​
​ ​>>>​​ ​​multiply_list([1,​​ ​​2,​​ ​​3],​​ ​​0)​
​ Traceback (most recent call last):
​  File "<python-input-12>", line 1, in <module>
​  multiply_list([1, 2, 3], 0)
​  ~~~~~~~~~~~~~^^^^^^^^^^^
​  File "<python-input-11>", line 3, in multiply_list
​  raise ValueError
​ ValueError

Here, ValueError is the appropriate exception to raise, not TypeError. We assume that n has the correct type (int), but its value is invalid. If the type itself is incorrect, Python’s multiplication operator will raise a TypeError on its own.

You can also pass arguments to an exception when you raise it. These arguments are stored in the exception object’s args attribute (see ) and provide additional context during debugging:

​ ​>>>​​ ​​def​​ ​​multiply_list(l:​​ ​​list,​​ ​​n:​​ ​​int)​​ ​​->​​ ​​list:​
​ ​...​​ ​​"""Return list `l` replicated `n` times."""​
​ ​...​​ ​​if​​ ​​n​​ ​​<=​​ ​​0:​
​ ​...​​ ​​raise​​ ​​ValueError(n)​
​ ​...​​ ​​return​​ ​​n​​ ​​*​​ ​​l​
​ ​...​
​ ​>>>​​ ​​multiply_list([1,​​ ​​2,​​ ​​3],​​ ​​-10)​
​ Traceback (most recent call last):
​  File "<python-input-26>", line 1, in <module>
​  multiply_list([1, 2, 3], -10)
​  ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^
​  File "<python-input-25>", line 3, in multiply_list
​  raise ValueError(n)
​ ValueError: -10
Exception chaining preserves the original cause of an error, giving you better insight for debugging

Beyond simple error reporting, raise can also be used for exception chaining: a mechanism that lets you preserve the original cause of an error while raising a new, more abstract, higher-level one. This is especially useful when translating low-level exceptions into higher-level errors without losing valuable debugging information.

Consider the following example. The function load_data tries to open a file and anticipates a FileNotFoundError. Instead of handling that exception directly, it raises a new exception, IOError, with an additional message argument, chaining the new exception to the original:

​ ​>>>​​ ​​def​​ ​​load_data(filename:​​ ​​str)​​ ​​->​​ ​​str:​
​ ​...​​ ​​"""Return the contents of the file `filename`."""​
​ ​...​​ ​​try:​
​ ​...​​ ​​with​​ ​​open(filename)​​ ​​as​​ ​​infile:​
​ ​...​​ ​​return​​ ​​infile.read()​
​ ​...​​ ​​except​​ ​​FileNotFoundError​​ ​​as​​ ​​eo:​
​ ​...​​ ​​raise​​ ​​IOError(f​​'Could not load data from {filename}'​​)​​ ​​from​​ ​​eo​
​ ​...​
​ ​>>>​​ ​​import​​ ​​traceback​
​ ​...​​ ​​try:​
​ ​...​​ ​​load_data(​​'missing.txt'​​)​
​ ​...​​ ​​except​​ ​​IOError​​ ​​as​​ ​​eo:​
​ ​...​​ ​​print(eo)​
​ ​...​​ ​​print(f​​'Original cause: {eo.__cause__}'​​)​
​ ​...​​ ​​traceback.print_exception(eo)​
​ ​...​
​ Could not load data from missing.txt
​ Original cause: [Errno 2] No such file or directory: 'missing.txt'
​ Traceback (most recent call last):
​  File "<python-input-2>", line 3, in load_data
​  with open(filename) as infile:
​  ~~~~^^^^^^^^^^
​ FileNotFoundError: [Errno 2] No such file or directory: 'missing.txt'
​ 
​ The above exception was the direct cause of the following exception:
​ 
​ Traceback (most recent call last):
​  File "<python-input-5>", line 3, in <module>
​  load_data('missing.txt')
​  ~~~~~~~~~^^^^^^^^^^^^^^^
​  File "<python-input-2>", line 6, in load_data
​  raise IOError(f'Could not load data from {filename}') from eo
​ OSError: Could not load data from missing.txt

This design accomplishes three important goals:

The from eo syntax in the raise statement copies details from the original exception object (eo) into the new one, where you can access them through the __cause__ attribute. The function traceback.print_exception shows the full call stack for post-mortem analysis.

Use exception chaining when you want to abstract away implementation details from the caller but still retain them for debugging, logging, or troubleshooting.

Назад: Common Built-in Exceptions
Дальше: Defining Custom Exceptions