Zum Inhalt springen
aviral gupta

// I1.2 · ca. 30 Min. · Aufbau

Verkettete Exceptions

Nach dieser Lektion lesen Sie einen Traceback mit zwei Exceptions, übersetzen einen technischen Fehler in Ihren eigenen, ohne ihn zu verlieren, und blenden einen nutzlosen Ursprungsfehler aus.

Lektion 2 von 5 in I1 Fehler im Detail

Danach können Sie

  • Einen verketteten Traceback lesen und "During handling" von "direct cause" unterscheiden
  • Einen Fehler mit raise … from err übersetzen und das Original in __cause__ und __context__ finden
  • Einen wenig hilfreichen Ursprungsfehler mit from None ausblenden, obwohl er erhalten bleibt
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus der letzten Lektion: class ConfigError(AppError) und class AppError(Exception). Welche Klauseln fangen einen ConfigError ab? Wählen Sie alle zutreffenden.

    Wählen Sie alle zutreffenden aus.

  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es vorher, bevor Sie weiterlesen: Das endet mit einem Traceback, der zwei Exceptions zeigt. Welche Zeile verbindet sie?

    settings = {}
    try:
        port = settings["port"]
    except KeyError:
        raise RuntimeError("config has no port")
  3. Üben · Aufgabe 3 von 7

    Setzen Sie das Schlüsselwort ein, das den KeyError zur direkten Ursache des ConfigError macht.

    raise ConfigError("port is missing") ____ err
    raise ConfigError("port is missing") err
  4. Üben · Aufgabe 4 von 7

    Was gibt das aus?

    try:
        try:
            1 / 0
        except ZeroDivisionError as err:
            raise ValueError("bad ratio") from err
    except ValueError as exc:
        print(type(exc.__cause__).__name__, type(exc.__context__).__name__)
  5. Üben · Aufgabe 5 von 7

    Ein ValueError wird in except KeyError ausgelöst. Ordnen Sie jedem raise zu, was sein Traceback zeigt.

  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. from None blendet den KeyError aus. Ist er weg? Was gibt das aus?

    def lookup(prices, item):
        try:
            return prices[item]
        except KeyError:
            raise LookupError(f"no price for {item}") from None
    
    
    try:
        lookup({}, "tea")
    except LookupError as err:
        print(err.__cause__, repr(err.__context__))
  7. Anwenden · Aufgabe 7 von 7

    Mini-Aufgabe. Definieren Sie PriceError und schreiben Sie parse_price(text), das float(text) zurückgibt. Scheitert float, lösen Sie PriceError mit der Meldung bad price: und dem Text in repr-Form aus, mit from an den ValueError gekettet. Rufen Sie es mit "2.50" und "two" auf und geben Sie beim Fehler die Meldung und repr(err.__cause__) aus.

    Prüfen Sie Ihr Ergebnis anhand dieser Liste

Selbst programmieren

Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.

Ausgearbeitetes Beispiel

Einstellungen laden: übersetzen, ausblenden oder durchlassen

load_port liest einen Port aus JSON-Text. Ungültiges JSON übersetzt es mit from err in SettingsError, sodass der Parse-Fehler als Ursache sichtbar bleibt. Ein fehlender Schlüssel wird mit from None zu SettingsError, weil der KeyError nichts beiträgt. Ein Port, der keine Zahl ist, wird gar nicht behandelt: Sein ValueError erreicht den Aufrufer unverändert.

main.py

import json


class SettingsError(Exception):
    pass


def load_port(text: str) -> int:
    try:
        data = json.loads(text)
    except json.JSONDecodeError as err:
        raise SettingsError("settings are not valid JSON") from err
    try:
        return int(data["port"])
    except KeyError:
        raise SettingsError("settings have no port") from None


for text in ['{"port": "8080"}', '{"port": 80', '{"host": "a"}', '{"port": "http"}']:
    try:
        port = load_port(text)
    except SettingsError as err:
        cause = type(err.__cause__).__name__
        context = type(err.__context__).__name__
        print(f"{err} | cause: {cause} | context: {context}")
    except ValueError as err:
        print("not a SettingsError:", err)
    else:
        print("port", port)

Ausführen mit

python main.py

Ausgabe

port 8080
settings are not valid JSON | cause: JSONDecodeError | context: JSONDecodeError
settings have no port | cause: NoneType | context: KeyError
not a SettingsError: invalid literal for int() with base 10: 'http'
  • Mit from err sind __cause__ und __context__ beide der JSONDecodeError.
  • Mit from None ist __cause__ None, aber der KeyError steckt weiter in __context__.
  • except KeyError fängt den ValueError aus int("http") nicht ab, er geht unübersetzt durch.
  • Der Aufrufer fängt SettingsError ab und muss nichts über json oder KeyError wissen.
Ändern und ausführen

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.

Übungen

Übung 1 von 2

Die Ursache behalten

parse_price(text) löst bereits PriceError aus, wenn float den Text nicht lesen kann, und bei einem negativen Preis. Ändern Sie es so, dass der "bad price"-PriceError mit from an den ValueError gekettet ist, sodass err.__cause__ dieser ValueError ist. Der Fehler für negative Preise hat keine Ursache und bleibt, wie er ist.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Um den ValueError zu verwenden, geben Sie ihm einen Namen: except ValueError as err:.

  2. Hinweis 2

    Ergänzen Sie dann from err am Ende der raise-Zeile.

  3. Hinweis 3

    raise PriceError(f"bad price: {text!r}") from err

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

class PriceError(Exception):
    pass


def parse_price(text: str) -> float:
    try:
        value = float(text)
    except ValueError as err:
        raise PriceError(f"bad price: {text!r}") from err
    if value < 0:
        raise PriceError(f"negative price: {value}")
    return value
Auf dem eigenen Computer ausführen

Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.py

class PriceError(Exception):
    pass


def parse_price(text: str) -> float:
    try:
        value = float(text)
    except ValueError:
        raise PriceError(f"bad price: {text!r}")
    if value < 0:
        raise PriceError(f"negative price: {value}")
    return value

test_main.py

from main import PriceError, parse_price


def test_valid():
    """Ein gültiger Preis kommt als float zurück"""
    got = parse_price("2.50")
    assert got == 2.5, f"parse_price('2.50') lieferte {got!r}"


def test_bad_price_message():
    """Text, der keine Zahl ist, löst PriceError mit klarer Meldung aus"""
    try:
        parse_price("two")
    except PriceError as err:
        assert str(err) == "bad price: 'two'", f"die Meldung war {str(err)!r}"
    else:
        assert False, "parse_price('two') kehrte normal zurück"


def test_bad_price_cause():
    """Der ValueError ist die direkte Ursache"""
    try:
        parse_price("two")
    except PriceError as err:
        assert isinstance(err.__cause__, ValueError), f"__cause__ ist {err.__cause__!r}; verwenden Sie raise ... from err"


def test_negative():
    """Ein negativer Preis löst PriceError ohne Ursache aus"""
    try:
        parse_price("-1")
    except PriceError as err:
        assert str(err) == "negative price: -1.0", f"die Meldung war {str(err)!r}"
        assert err.__cause__ is None, f"__cause__ ist {err.__cause__!r}, erwartet: None"
    else:
        assert False, "parse_price('-1') kehrte normal zurück"

Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.

Programm ausführen:

python main.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Übung 2 von 2

Den Zugriff ausblenden, die Umwandlung behalten

Schreiben Sie get_setting(settings, key). Es liefert settings[key]. Fehlt der Schlüssel, lösen Sie SettingError(f"unknown setting: {key!r}") from None aus. Schreiben Sie dann get_int_setting(settings, key), das int(get_setting(settings, key)) liefert. Scheitert int, lösen Sie SettingError(f"setting {key!r} is not a whole number") aus, mit from an den ValueError gekettet.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Setzen Sie in get_setting return settings[key] in ein try und schreiben Sie in except KeyError: raise SettingError(...) from None.

  2. Hinweis 2

    Rufen Sie in get_int_setting get_setting außerhalb des try auf, damit sein SettingError nicht erneut abgefangen wird. Nur int(text) gehört ins try.

  3. Hinweis 3

    except ValueError as err: raise SettingError(f"setting {key!r} is not a whole number") from err

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

class SettingError(Exception):
    pass


def get_setting(settings: dict[str, str], key: str) -> str:
    try:
        return settings[key]
    except KeyError:
        raise SettingError(f"unknown setting: {key!r}") from None


def get_int_setting(settings: dict[str, str], key: str) -> int:
    text = get_setting(settings, key)
    try:
        return int(text)
    except ValueError as err:
        raise SettingError(f"setting {key!r} is not a whole number") from err
Auf dem eigenen Computer ausführen

Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.py

class SettingError(Exception):
    pass


def get_setting(settings: dict[str, str], key: str) -> str:
    return settings[key]


def get_int_setting(settings: dict[str, str], key: str) -> int:
    return int(get_setting(settings, key))

test_main.py

from main import SettingError, get_int_setting, get_setting

SETTINGS = {"port": "8080", "host": "example.org"}


def catch(f, key):
    try:
        f(SETTINGS, key)
    except SettingError as err:
        return err
    assert False, f"{f.__name__}(settings, {key!r}) kehrte normal zurück"


def test_values():
    """Vorhandene Einstellungen kommen zurück"""
    got = get_setting(SETTINGS, "host"), get_int_setting(SETTINGS, "port")
    assert got == ("example.org", 8080), f"Ergebnis: {got!r}"


def test_unknown_hidden():
    """Ein unbekannter Schlüssel löst SettingError from None aus"""
    err = catch(get_setting, "user")
    assert str(err) == "unknown setting: 'user'", f"die Meldung war {str(err)!r}"
    assert err.__cause__ is None and err.__suppress_context__, "der KeyError sollte mit from None ausgeblendet werden"
    assert isinstance(err.__context__, KeyError), f"__context__ ist {err.__context__!r}, erwartet: der KeyError"


def test_not_a_number_chained():
    """Eine Einstellung, die keine Zahl ist, wird an den ValueError gekettet"""
    err = catch(get_int_setting, "host")
    assert str(err) == "setting 'host' is not a whole number", f"die Meldung war {str(err)!r}"
    assert isinstance(err.__cause__, ValueError), f"__cause__ ist {err.__cause__!r}; verwenden Sie raise ... from err"


def test_unknown_int():
    """get_int_setting meldet einen unbekannten Schlüssel genauso"""
    err = catch(get_int_setting, "user")
    assert str(err) == "unknown setting: 'user'", f"die Meldung war {str(err)!r}"

Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.

Programm ausführen:

python main.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Häufige Fehler

Ein String nach from

raise RuntimeError("could not load") from "file missing"

Was Python ausgibt

TypeError: exception causes must derive from BaseException

Warum, und die Lösung

Die Ursache muss ein Exception-Objekt sein (oder None), keine Meldung. Fangen Sie das Original mit except OSError as err: ab und schreiben Sie raise RuntimeError("could not load") from err. Zusätzliche Worte gehören in die Meldung der neuen Exception.

Den as-Namen nach der except-Klausel benutzen

try:
    int("x")
except ValueError as err:
    print("bad number")
print(err)

Was Python ausgibt

NameError: name 'err' is not defined

Warum, und die Lösung

Python löscht den Namen nach as, sobald die except-Klausel endet. Wollen Sie die Exception später verwenden, etwa zum Verketten oder Melden, weisen Sie sie in der Klausel einer anderen Variablen zu: saved = err.

Ein Fehler im Handler selbst

prices = {"tea": 2.5}
try:
    price = prices["coffee"]
except KeyError:
    price = fallbak_price

Was Python ausgibt

NameError: name 'fallbak_price' is not defined

Warum, und die Lösung

Der Traceback zeigt den KeyError, dann During handling of the above exception, another exception occurred:, dann diesen NameError. Diese Verbindungszeile heißt: Der Handler selbst ist gescheitert, hier an einem falsch geschriebenen Namen. Korrigieren Sie den Handler, nicht den try-Block.

Python im Browser: Pyodide 314.0.7, MPL-2.0. Lizenz und Quellcode

Abschlussquiz

5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.

Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

Automatische Verkettung: ein Fehler beim Behandeln eines Fehlers

Wird in einer except-Klausel eine neue Exception ausgelöst, hängt Python die gerade behandelte Exception an, im Attribut __context__. Der Traceback zeigt dann beide: zuerst das Original, dann die Zeile During handling of the above exception, another exception occurred:, dann die neue. Die letzte Zeile eines Tracebacks ist immer die neueste Exception. Wollten Sie im Handler gar nichts auslösen, deutet das auf einen Fehler dort hin, etwa einen falsch geschriebenen Namen.

raise … from err: eine gewollte Ursache

raise ConfigError("port is missing") from err sagt: Dieser Fehler ist eine direkte Folge von err. Python speichert err in __cause__ und in __context__, und der Traceback verbindet beide mit The above exception was the direct cause of the following exception:. So übersetzen Sie Fehler: Aufrufer fangen Ihren ConfigError statt eines KeyError aus der Tiefe, und Entwickler sehen trotzdem den Ursprung. Nach from steht eine Exception oder None. Der Name nach as gilt nur innerhalb der except-Klausel; speichern Sie das Objekt bei Bedarf in einer anderen Variablen.

from None: das Rauschen ausblenden

raise ValueError("unknown item: cake") from None zeigt nur die neue Exception. Es setzt __suppress_context__ auf True, sodass der Traceback das Original weglässt; in __context__ bleibt es aber für die Fehlersuche erhalten. Nutzen Sie es, wenn der erste Fehler ein internes Detail ist: Ein KeyError aus Ihrem eigenen Dict-Zugriff sagt weniger als eine klare Meldung. Verstecken Sie damit keine Fehler, die Sie nicht verstehen; das macht Bugs schwerer zu finden.

Quellen

Zuletzt geprüft am 29. September 2026