Python Exception Chaining (Implicit and Explicit)
In Python, when exceptions occur, errors can be raised elsewhere in the code while preserving the original exception. This is particularly useful when you want to catch an exception but need to raise a new, more specific exception.
Bernd Fischer
18. Oct 2024 · 3 min read
Exception Chaining
Exception chaining in Python is a useful tool to link errors in a clear and traceable manner.
The choice between implicit and explicit chaining depends on whether you want to explicitly handle the original exception or not. In any case, Python provides a way to make error situations transparent and their cause easy to understand through exception chaining.
Implicit Exception Chaining
Implicit exception chaining occurs when a new exception is raised within a except block without explicitly referencing the original exception. Python automatically links the original exception with the new exception via the attribute __context__.
Example 1: Implicit Chaining
try:
1 / 0 # Löst ZeroDivisionError aus
except ZeroDivisionError:
raise ValueError("Ein Fehler trat auf.") # Löst ValueError aus
In this example, a ZeroDivisionError exception first occurs. However, inside the except block, a new exception, a ValueError, is raised. Python links the original error with the new one, and the original exception is accessible via the attribute __context__.
When you run the program, the error traceback is displayed, including the original error:
Traceback (most recent call last):
File "example.py", line 2, in <module>
1 / 0
ZeroDivisionError: division by zero
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "example.py", line 4, in <module>
raise ValueError("Ein Fehler trat auf.")
ValueError: Ein Fehler trat auf.
Example 2: Implicit Chaining with Multiple Exceptions
try:
numbers = [1, 2, 3]
print(numbers[5]) # Löst IndexError aus
except IndexError:
raise KeyError("Falscher Schlüssel.")
Here, a IndexError occurs first because of an attempt to access a nonexistent list element. Then a KeyError is raised, chained with the original error:
Traceback (most recent call last):
File "example.py", line 3, in <module>
print(numbers[5])
IndexError: list index out of range
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "example.py", line 5, in <module>
raise KeyError("Falscher Schlüssel.")
KeyError: 'Falscher Schlüssel.'
Explicit Exception Chaining
With explicit exception chaining, you can explicitly reference the original exception when raising a new exception. This is done using the keyword from. This links the new exception to the old one, allowing you to explicitly specify which exception caused the issue.
Example 1: Explicit Exception Chaining
try:
open("nicht_existierende_datei.txt") # Löst FileNotFoundError aus
except FileNotFoundError as e:
raise RuntimeError("Fehler beim Öffnen der Datei.") from e
In this example, a FileNotFoundError occurs because the file does not exist. We catch this exception and explicitly raise a new RuntimeError exception, linked to the original exception FileNotFoundError:
Traceback (most recent call last):
File "example.py", line 2, in <module>
open("nicht_existierende_datei.txt")
FileNotFoundError: [Errno 2] No such file or directory: 'nicht_existierende_datei.txt'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "example.py", line 4, in <module>
raise RuntimeError("Fehler beim Öffnen der Datei.") from e
RuntimeError: Fehler beim Öffnen der Datei.
Here, the FileNotFoundError is explicitly given as the cause of the RuntimeError.
Example 2: Exception Chaining with Multiple Levels
Sometimes an exception can be chained multiple times, e.g., when several exceptions occur at different levels of a program. The following example shows how to create a chain of exceptions:
try:
raise ValueError("Ursprünglicher Fehler.")
except ValueError as e:
try:
raise TypeError("Ein weiterer Fehler.") from e
except TypeError as te:
raise RuntimeError("Endgültiger Fehler.") from te
A chain of exceptions occurs here: first ValueError, then TypeError, and finally RuntimeError, all linked together.
Traceback (most recent call last):
File "example.py", line 4, in <module>
raise ValueError("Ursprünglicher Fehler.")
ValueError: Ursprünglicher Fehler.
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "example.py", line 7, in <module>
raise TypeError("Ein weiterer Fehler.") from e
TypeError: Ein weiterer Fehler.
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "example.py", line 9, in <module>
raise RuntimeError("Endgültiger Fehler.") from te
RuntimeError: Endgültiger Fehler.
context and cause
In Python, there are two attributes related to exception chaining: __context__ and __cause__. Both play roles in linking exceptions, but are used in different situations.
Here are the differences:
__context__:
- Set when an exception occurs inside a
exceptblock (implicit exception chaining). - Python automatically fills
__context__when you raise a new exception while catching another. __context__points to the original exception that was not explicitly specified.
__cause__:
- Used when explicit exception chaining occurs with
raise ... from .... - When you write
raise NewException from OldException, the old exception (OldException) is stored in the__cause__attribute of the new exception (NewException). __cause__explicitly highlights the chaining and gives you more control over the exception cause.
Example for __context__
This shows how __context__ is used in a case of implicit exception chaining:
try:
try:
1 / 0 # Löst ZeroDivisionError aus
except ZeroDivisionError:
raise ValueError("Fehler in der Berechnung") # Neue Ausnahme wird ausgelöst
except ValueError as e:
print("ValueError aufgetreten:", e)
if e.__context__:
print("Ursprüngliche Ausnahme (__context__):", e.__context__)
Explanation:
ZeroDivisionErroris raised in the innertryblock.- In the
exceptblock, a new exceptionValueErroris raised. - Python implicitly links the new exception with the original exception and stores the
ZeroDivisionErrorin__context__.
Output:
ValueError aufgetreten: Fehler in der Berechnung
Ursprüngliche Ausnahme (__context__): division by zero
Example for __cause__
In the following example, we use explicit exception chaining with raise ... from ...:
try:
try:
open("nicht_existierende_datei.txt") # Löst FileNotFoundError aus
except FileNotFoundError as e:
raise RuntimeError("Fehler beim Öffnen der Datei") from e
except RuntimeError as e:
print("RuntimeError aufgetreten:", e)
if e.__cause__:
print("Ursprüngliche Ausnahme (__cause__):", e.__cause__)
Explanation:
FileNotFoundErroris raised because the file does not exist.- A new exception
RuntimeErroris explicitly linked withfrom. - The original exception
FileNotFoundErroris stored in__cause__.
Output:
RuntimeError aufgetreten: Fehler beim Öffnen der Datei
Ursprüngliche Ausnahme (__cause__): [Errno 2] No such file or directory: 'nicht_existierende_datei.txt'