Zum Inhalt springen
aviral gupta

// I1.4 · ca. 35 Min. · Aufbau

Exception-Gruppen und Notizen

Nach dieser Lektion sammeln Sie mehrere Fehler und lösen sie gemeinsam aus, behandeln jede Art mit except* und hängen Notizen an, die sagen, wo ein Fehler passiert ist.

Lektion 4 von 5 in I1 Fehler im Detail

Danach können Sie

  • Fehler in einer Schleife sammeln, als ExceptionGroup auslösen und ihren Traceback lesen
  • Teile einer Gruppe mit except* behandeln und vorhersagen, welche Klauseln laufen und was übrig bleibt
  • Einer Exception mit add_note Kontext geben und die Notizen in __notes__ und im Traceback finden
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus der letzten Lektion: Der try-Block löst eine Exception aus, zu der keine except-Klausel passt. Was passiert trotzdem, bevor sie beim Aufrufer ankommt? 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 hier löst zwei Fehler gemeinsam aus. Eine Zeile des Tracebacks beschreibt die Gruppe selbst. Welche?

    errors = [ValueError("age must be a number"), KeyError("email")]
    raise ExceptionGroup("2 records failed", errors)
  3. Üben · Aufgabe 3 von 7

    Setzen Sie die Klasse ein, die alle Fehler der Liste gemeinsam auslöst.

    raise ____("3 rows failed", errors)
    raise ("3 rows failed", errors)
  4. Üben · Aufgabe 4 von 7

    Eine Gruppe, zwei except*-Klauseln. Was gibt das aus?

    try:
        raise ExceptionGroup("batch", [ValueError("a"), KeyError("b"), ValueError("c")])
    except* ValueError as eg:
        print("values:", len(eg.exceptions), end="; ")
    except* KeyError as eg:
        print("keys:", len(eg.exceptions), end="; ")
  5. Üben · Aufgabe 5 von 7

    eg = ExceptionGroup("batch", [ValueError("a"), KeyError("b")]). Ordnen Sie jedem Ausdruck seinen Wert zu.

  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Nur der ValueError hat eine except*-Klausel. Was gibt das aus?

    try:
        try:
            raise ExceptionGroup("batch", [ValueError("a"), KeyError("b")])
        except* ValueError:
            print("handled the ValueError", end=" | ")
    except ExceptionGroup as eg:
        print("left over:", eg.exceptions)
  7. Anwenden · Aufgabe 7 von 7

    Mini-Aufgabe. Schreiben Sie check_prices(prices); prices ordnet einem Namen einen Text wie "2.5" zu. Wandeln Sie jeden mit float um und lehnen Sie negative Preise ab. Hören Sie nicht beim ersten Problem auf: Geben Sie jedem Fehler eine Notiz mit dem Namen, sammeln Sie sie und lösen Sie am Ende eine ExceptionGroup aus. Behandeln Sie sie mit except* ValueError und geben Sie jede Meldung mit ihrer Notiz 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

Einen Stapel Anmeldungen prüfen

check_all prüft jede Anmeldung und hört nicht bei der ersten schlechten auf. Jeder Fehler bekommt eine Notiz mit seiner Datensatznummer und landet in einer Liste, und am Ende trägt eine ExceptionGroup sie alle. Der Aufrufer behandelt falsche Werte und fehlende Felder in getrennten except*-Klauseln und gibt jeden Fehler mit seiner Notiz aus.

main.py

def check(record: dict[str, str]) -> None:
    if "email" not in record:
        raise KeyError("email")
    if not record["age"].isdigit():
        raise ValueError(f"age must be a number, got {record['age']!r}")


def check_all(records: list[dict[str, str]]) -> None:
    errors: list[Exception] = []
    for number, record in enumerate(records, start=1):
        try:
            check(record)
        except (ValueError, KeyError) as err:
            err.add_note(f"in record {number}")
            errors.append(err)
    if errors:
        raise ExceptionGroup(f"{len(errors)} of {len(records)} records failed", errors)


records = [
    {"age": "34", "email": "ada@example.org"},
    {"age": "x", "email": "bob@example.org"},
    {"age": "51"},
    {"age": "-3", "email": "eve@example.org"},
]

try:
    check_all(records)
except* ValueError as bad_values:
    print(bad_values.message)
    for err in bad_values.exceptions:
        print("bad value:", err, "|", err.__notes__[0])
except* KeyError as missing:
    for key_err in missing.exceptions:
        print("missing field:", key_err, "|", key_err.__notes__[0])

Ausführen mit

python main.py

Ausgabe

3 of 4 records failed
bad value: age must be a number, got 'x' | in record 2
bad value: age must be a number, got '-3' | in record 4
missing field: 'email' | in record 3
  • Die Meldung der Gruppe bleibt in der kleineren Gruppe erhalten, die jede except*-Klausel bekommt.
  • Beide ValueErrors kommen zusammen in einem Durchlauf der ersten except*-Klausel an.
  • Nachdem die ValueErrors behandelt sind, bleibt der KeyError übrig, also läuft auch die zweite Klausel.
  • str(err) ist nur die Meldung; die Notiz steht in err.__notes__.
Ä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

Sammeln statt abbrechen

parse_all(texts) wandelt eine Liste von Texten in ints um, bricht aber beim ersten schlechten ab. Lassen Sie die Funktion jeden Text prüfen: Geben Sie jedem ValueError die Notiz "item <n>", gezählt ab 1, sammeln Sie sie und lösen Sie nach der Schleife ExceptionGroup("some items are not numbers", errors) aus. Sind alle Texte in Ordnung, geben Sie die Liste der ints zurück.

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

    Gehen Sie mit enumerate(texts, start=1) durch die Liste und setzen Sie int(text) in ein try innerhalb der Schleife.

  2. Hinweis 2

    In except ValueError as err: rufen Sie err.add_note(f"item {n}") auf und hängen err an eine Fehlerliste.

  3. Hinweis 3

    Nach der Schleife: if errors: raise ExceptionGroup("some items are not numbers", errors). Sonst geben Sie die Zahlen zurück.

Eine Lösung zeigen

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

def parse_all(texts: list[str]) -> list[int]:
    numbers: list[int] = []
    errors: list[ValueError] = []
    for n, text in enumerate(texts, start=1):
        try:
            numbers.append(int(text))
        except ValueError as err:
            err.add_note(f"item {n}")
            errors.append(err)
    if errors:
        raise ExceptionGroup("some items are not numbers", errors)
    return numbers
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

def parse_all(texts: list[str]) -> list[int]:
    return [int(text) for text in texts]

test_main.py

from main import parse_all


def test_all_good():
    """Gute Texte kommen als ints zurück"""
    got = parse_all(["1", "22", "-3"])
    assert got == [1, 22, -3], f"parse_all lieferte {got!r}"


def catch(texts):
    try:
        parse_all(texts)
    except ExceptionGroup as eg:
        return eg
    except ValueError:
        assert False, "parse_all brach beim ersten ValueError ab; sammeln Sie sie in einer ExceptionGroup"
    assert False, "parse_all kehrte bei schlechter Eingabe normal zurück"


def test_group():
    """Zwei schlechte Texte ergeben eine Gruppe mit beiden Fehlern"""
    eg = catch(["1", "x", "3", "y"])
    assert eg.message == "some items are not numbers", f"die Meldung der Gruppe ist {eg.message!r}"
    assert len(eg.exceptions) == 2, f"die Gruppe enthält {len(eg.exceptions)} Exceptions, erwartet: 2"
    assert all(isinstance(e, ValueError) for e in eg.exceptions), "jede Exception in der Gruppe sollte ein ValueError sein"


def test_notes():
    """Jeder Fehler hat eine Notiz mit seiner Position"""
    eg = catch(["1", "x", "3", "y"])
    notes = [getattr(e, "__notes__", []) for e in eg.exceptions]
    assert notes == [["item 2"], ["item 4"]], f"die Notizen sind {notes!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

Übung 2 von 2

Die Gruppe sortieren

check_all ist fertig: Es löst eine ExceptionGroup aus ValueErrors und KeyErrors aus, jeder mit einer Notiz. Schreiben Sie summarize(records), das eine Liste von Zeilen liefert: "bad value: <message> (<note>)" für jeden ValueError, dann "missing field: <key> (<note>)" für jeden KeyError, mit except*. Löst check_all nichts aus, liefern Sie ["all valid"]. Andere Fehler lassen Sie durch.

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

    Beginnen Sie mit einer leeren Liste lines, dann try: check_all(records) mit zwei except*-Klauseln: except* ValueError as bad_values: und except* KeyError as missing:.

  2. Hinweis 2

    Gehen Sie in jeder Klausel die Exceptions der Gruppe durch und hängen Sie einen f-String mit dem Fehler und seinem __notes__[0] an. Nutzen Sie in der zweiten Klausel neue Namen, sonst beschwert sich mypy. return ist in except* verboten.

  3. Hinweis 3

    Nach der try-Anweisung: return lines or ["all valid"]. Eine leere Liste ist falsch.

Eine Lösung zeigen

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

def check(record: dict[str, str]) -> None:
    if "email" not in record:
        raise KeyError("email")
    if not record["age"].isdigit():
        raise ValueError(f"age must be a number, got {record['age']!r}")


def check_all(records: list[dict[str, str]]) -> None:
    errors: list[Exception] = []
    for number, record in enumerate(records, start=1):
        try:
            check(record)
        except (ValueError, KeyError) as err:
            err.add_note(f"in record {number}")
            errors.append(err)
    if errors:
        raise ExceptionGroup(f"{len(errors)} records failed", errors)


def summarize(records: list[dict[str, str]]) -> list[str]:
    lines: list[str] = []
    try:
        check_all(records)
    except* ValueError as bad_values:
        for err in bad_values.exceptions:
            lines.append(f"bad value: {err} ({err.__notes__[0]})")
    except* KeyError as missing:
        for key_err in missing.exceptions:
            lines.append(f"missing field: {key_err} ({key_err.__notes__[0]})")
    return lines or ["all valid"]
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

def check(record: dict[str, str]) -> None:
    if "email" not in record:
        raise KeyError("email")
    if not record["age"].isdigit():
        raise ValueError(f"age must be a number, got {record['age']!r}")


def check_all(records: list[dict[str, str]]) -> None:
    errors: list[Exception] = []
    for number, record in enumerate(records, start=1):
        try:
            check(record)
        except (ValueError, KeyError) as err:
            err.add_note(f"in record {number}")
            errors.append(err)
    if errors:
        raise ExceptionGroup(f"{len(errors)} records failed", errors)


def summarize(records: list[dict[str, str]]) -> list[str]:
    check_all(records)
    return ["all valid"]

test_main.py

from main import summarize


def test_all_valid():
    """Gültige Datensätze ergeben all valid"""
    got = summarize([{"age": "34", "email": "ada@example.org"}])
    assert got == ["all valid"], f"summarize lieferte {got!r}"


def test_both_kinds():
    """Erst die falschen Werte, dann die fehlenden Felder, jeweils mit Notiz"""
    records = [{"age": "x", "email": "a@b"}, {"age": "3"}, {"age": "-3", "email": "c@d"}]
    got = summarize(records)
    want = [
        "bad value: age must be a number, got 'x' (in record 1)",
        "bad value: age must be a number, got '-3' (in record 3)",
        "missing field: 'email' (in record 2)",
    ]
    assert got == want, f"summarize lieferte {got!r}"


def test_only_missing():
    """Auch eine Gruppe nur aus KeyErrors wird behandelt"""
    got = summarize([{"age": "1"}, {"age": "2"}])
    assert got == ["missing field: 'email' (in record 1)", "missing field: 'email' (in record 2)"], f"summarize lieferte {got!r}"


def test_other_errors_pass():
    """Andere Fehler werden nicht gefangen"""
    try:
        summarize([{"age": 5, "email": "a@b"}])  # type: ignore[dict-item]
    except AttributeError:
        pass
    else:
        assert False, "summarize hat einen AttributeError verschluckt"

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

except und except* gemischt

try:
    raise ExceptionGroup("batch", [ValueError("a")])
except* ValueError:
    print("values")
except KeyError:
    print("keys")

Was Python ausgibt

SyntaxError: cannot have both 'except' and 'except*' on the same 'try'

Warum, und die Lösung

Eine try-Anweisung nutzt nur eine Art Klausel. In einer Gruppe fangen Sie auch den KeyError mit except* KeyError:. Wollen Sie stattdessen die ganze Gruppe auf einmal fangen, nutzen Sie ein einfaches except ExceptionGroup: in einer eigenen try-Anweisung.

except* mit ExceptionGroup

try:
    raise ExceptionGroup("batch", [ValueError("a")])
except* ExceptionGroup:
    print("caught")

Was Python ausgibt

TypeError: catching ExceptionGroup with except* is not allowed. Use except instead.

Warum, und die Lösung

except* schaut sich die Exceptions in einer Gruppe an, der Gruppentyp selbst hat dort keine klare Bedeutung, und Python lehnt ihn ab, sobald die Klausel erreicht wird. Um eine Gruppe als Ganzes zu fangen, schreiben Sie except ExceptionGroup as eg:.

return in except*

def load():
    try:
        raise ExceptionGroup("batch", [ValueError("a")])
    except* ValueError:
        return "handled"

Was Python ausgibt

SyntaxError: 'break', 'continue' and 'return' cannot appear in an except* block

Warum, und die Lösung

Nach dieser Klausel müssen eventuell noch weitere except*-Klauseln laufen, deshalb ist vorzeitiges Verlassen verboten. Speichern Sie das Ergebnis in der Klausel in einer Variablen und geben Sie es nach der try-Anweisung zurück.

Eine Notiz, die kein String ist

err = ValueError("bad age")
err.add_note(42)

Was Python ausgibt

TypeError: add_note() argument must be str, not int

Warum, und die Lösung

Notizen sind Text, der im Traceback erscheint, also nimmt add_note nur einen String. Bauen Sie ihn mit einem f-String: err.add_note(f"in record {42}").

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

ExceptionGroup: mehrere Fehler auf einmal

Wenn Sie 100 Datensätze prüfen, wollen Sie oft jedes Problem sehen, nicht nur das erste. Fangen Sie jeden Fehler in der Schleife, hängen Sie ihn an eine Liste und lösen Sie nach der Schleife ExceptionGroup("3 records failed", errors) aus. Die Liste muss Exception-Objekte enthalten, keine Klassen. Eine Gruppe ist selbst eine Exception, also fängt except Exception sie als Ganzes; eg.message ist ihre Meldung, eg.exceptions ein Tupel der Fehler. Ihr Traceback beginnt mit + Exception Group Traceback, nennt die Gruppe als ExceptionGroup: 3 records failed (3 sub-exceptions) und zeigt dann jeden Fehler in einem nummerierten Kasten.

except*: jede Art einzeln behandeln

except* ValueError as eg: holt die ValueErrors aus einer Gruppe. Der Block läuft einmal, und eg ist eine kleinere Gruppe mit genau diesen; über eg.exceptions gehen Sie sie durch. Anders als bei except können für eine Gruppe mehrere except*-Klauseln laufen, jede für ihre Art. Was keine Klausel getroffen hat, wird am Ende erneut ausgelöst, als Gruppe, wenn es als Gruppe kam. Eine einzelne Exception, die zu except* passt, kommt in einer Gruppe mit leerer Meldung an. Ein try nutzt entweder except oder except*, nie beides, und return, break und continue sind in except* verboten.

Notizen: Kontext, der später dazukommt

Die Meldung einer Exception steht fest, sobald sie erzeugt wird, oft tief in Code, der nicht weiß, an welchem Datensatz oder welcher Datei er arbeitet. Der Code, der sie fängt, weiß es, also kann er err.add_note("in record 3") aufrufen und den Fehler dann sammeln oder erneut auslösen. Notizen erscheinen im Traceback, eine pro Zeile nach der Meldung, in der Reihenfolge, in der sie hinzugefügt wurden. Sie liegen in err.__notes__, einer Liste, die das erste add_note anlegt. str(err) bleibt nur die Meldung, und eine Notiz muss ein String sein.

Quellen

Zuletzt geprüft am 29. September 2026