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

Bernd Fischer

18. Oct 2024 · 3 min read

Python Exception Chaining (Implicit and Explicit)

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 except block (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:

  • ZeroDivisionError is raised in the inner try block.
  • In the except block, a new exception ValueError is raised.
  • Python implicitly links the new exception with the original exception and stores the ZeroDivisionError in __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:

  • FileNotFoundError is raised because the file does not exist.
  • A new exception RuntimeError is explicitly linked with from.
  • The original exception FileNotFoundError is 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'