Exceptions (try/except/else/finally, raise, eigene Exceptions)

Track Python · PCAP-Fundament · ca. 50 Min.

Worum es geht

Fehlerbehandlung kennst du aus JS/TS mit try/catch/finally. Python hat dasselbe Grundgerüst, aber mit drei Unterschieden: Du fängst Fehler nach Typ (Klassenhierarchie), es gibt einen zusätzlichen else-Zweig, und du kannst nur Objekte von Exception-Klassen auslösen, nicht beliebige Werte. Laut Quelle ist das PCAP-Block 2: Exceptions mit 5 Fragen und 14 % (Stand der Abfrage 1. Oktober 2026, laut Glossar). Dazu ist es Alltag in jedem Backend: Retry, Validierung, eigene Fehlerklassen. Nach der Lektion kannst du den Ablauf von try/except/else/finally vorhersagen, Fehler richtig verketten und eigene Exceptions schreiben.

Ablauf: erst die Übersetzung JS/TS nach Python, dann fünf kleine Schritte mit lauffähigem Code, dann fünf Übungen.

Von JS/TS her gedacht

Thema JS/TS Python
Fehler auslösen throw mit beliebigem Wert (throw "x" geht) raise nur mit Exception-Objekt oder -Klasse
Fangen ein catch (e) für alles, Typ per instanceof prüfen mehrere except Typ, der erste passende gewinnt
Kein Fehler kein eigener Zweig else läuft nur, wenn try ohne Exception endete
Aufräumen finally finally (läuft auch nach return, break)
Weiterreichen throw e raise allein im except-Block
Ursache verketten new Error("x", { cause: e }) raise X from Y
Eigene Fehlerklasse class MyError extends Error class MyError(Exception)

Dieselbe Idee in beiden Sprachen, Typ prüfen:

// TypeScript: ein catch, dann per instanceof unterscheiden
try {
  JSON.parse("{kaputt");
} catch (e) {
  if (e instanceof SyntaxError) console.log("kein JSON");
  else throw e;
}

Der else throw e aus TS entfällt in Python: Was nicht zu einem except passt, läuft von selbst weiter nach oben.

Konzept in kleinen Schritten

Schritt 1: Ablauf von try, except, else, finally

  • try: Code, der einen Fehler auslösen kann.
  • except: fängt passende Exceptions. Mehrere Typen mit except (A, B):, das Objekt mit as e. Statt des Tupels darf dort auch eine Variable stehen, die ein Tupel von Klassen enthält (erlaubt = (A, B) und dann except erlaubt:). Das brauchst du in Übung 5.
  • else: läuft nur, wenn im try keine Exception auftrat.
  • finally: läuft immer, auch nach return, break oder einer nicht gefangenen Exception.

Die Ausgabe: für "4" kommt kein Fehler: 2.5 und immer, für "abc" kommt keine Zahl und immer, für "0" kommt durch null: division by zero und immer.

Warum else, wenn man den Code auch ans Ende von try schreiben könnte? Weil alles im try Fehler fangen darf. In else landen nur Fehler, die du nicht fangen willst.

Schritt 2: Hierarchie und Reihenfolge

Exceptions bilden eine Klassenhierarchie. Ein except mit einer Oberklasse fängt auch alle Unterklassen. Auszug aus der Quelle:

BaseException
 ├─ SystemExit
 ├─ KeyboardInterrupt
 ├─ GeneratorExit
 └─ Exception
     ├─ ArithmeticError
     │   ├─ ZeroDivisionError
     │   └─ OverflowError
     ├─ LookupError
     │   ├─ IndexError
     │   └─ KeyError
     ├─ ValueError
     ├─ TypeError
     ├─ NameError ... AttributeError ... OSError ...

Der erste passende except gewinnt. Darum zuerst die spezifischen, dann die allgemeinen Typen.

except Exception fängt fast alles, aber nicht KeyboardInterrupt und SystemExit. Ein nacktes except: fängt auch diese und gilt als schlechter Stil (bare except). Ein nützliches Detail: LookupError fängt KeyError und IndexError zusammen.

Schritt 3: raise, assert, Verkettung

  • raise ValueError("text") löst aus.
  • raise allein im except-Block reicht dieselbe Exception weiter (nach Protokollieren zum Beispiel).
  • raise Neu from Alt verkettet: Die Ursache steht danach in Neu.__cause__.
  • assert x > 0, "Meldung" löst einen AssertionError aus, wenn die Bedingung falsch ist.

e.args liefert die Argumente als Tupel, str(e) den Text. Anders als throw "x" in JS geht raise "text" nicht: Python meldet einen TypeError.

Schritt 4: Eigene Exceptions

Eine eigene Exception erbt von Exception. Mit __init__ und super().__init__(msg) gibst du Zusatzdaten mit.

Ohne eigenes __init__ reicht class MyError(Exception): pass. Erbst du von einer passenden Oberklasse wie ValueError, fangen auch bestehende except ValueError-Blöcke deinen Fehler mit.

Schritt 5: finally und die Fallen

Ein return im finally überschreibt ein return im try und verschluckt sogar eine laufende Exception. Wirft ein except-Block selbst eine neue Exception, läuft finally trotzdem, bevor sie nach oben geht:

Der Name nach as gilt nur im except-Block:

Falle: die Prüfungs- und Praxisklassiker

  1. Falsche Reihenfolge: except Exception vor except ZeroDivisionError, der spezifische Block wird nie erreicht.
  2. return im finally überschreibt das return im try und verschluckt Exceptions.
  3. Bare except: fängt auch KeyboardInterrupt und SystemExit.
  4. finally läuft immer, auch wenn der except-Block selbst eine neue Exception auslöst.
  5. as e gilt nur im Block: danach ist e gelöscht (NameError).
  6. Zu breit fangen: Ein except ValueError um zu viel Code fängt auch Fehler, die du gar nicht meinst. Wenn eine eigene Exception von ValueError erbt, gilt das erst recht.
  7. raise ohne Typ außerhalb eines except-Blocks hat nichts zum Weiterreichen (RuntimeError).

Übungen

Übung 1: Ablauf vorhersagen

Der Code steht fest, du musst nur das Ergebnis vorhersagen:

def ablauf(eingabe, log):
    try:
        log.append("try")
        zahl = int(eingabe)
        wert = 10 // zahl
    except ValueError:
        log.append("value")
        return "v"
    except ZeroDivisionError:
        log.append("zero")
        return "z"
    else:
        log.append("else")
        return wert
    finally:
        log.append("finally")

log = []
ergebnisse = [ablauf(x, log) for x in ["5", "0", "abc"]]

Trage das Tupel (ergebnisse, log) ein.

Gehe die drei Eingaben einzeln durch. Welche Zeile wirft jeweils zuerst, und welche Zweige laufen danach? Überlege auch, ob finally nach einem return noch etwas schreibt.

antwort = (
    [2, "z", "v"],
    ["try", "else", "finally", "try", "zero", "finally", "try", "value", "finally"],
)
antwort

Übung 2: Exception im except-Block (Multiple Choice)

log = []
try:
    try:
        1 / 0
    except ZeroDivisionError:
        log.append("a")
        raise ValueError("neu")
    finally:
        log.append("b")
except ValueError:
    log.append("c")

Welche Reihenfolge hat log danach? Gib den Buchstaben als String zurück.

  • A: ["a", "c", "b"]
  • B: ["b", "a", "c"]
  • C: ["a", "b", "c"]
  • D: ["c", "a", "b"]

Die neue ValueError entsteht im inneren except. Der äußere except ValueError kann sie erst fangen, wenn sie das innere try-Statement verlassen hat. Was muss das innere Statement vorher noch erledigen?

antwort = "C"
antwort

Übung 3: Eigene Exception und Verkettung

Schreibe die Exception-Klasse AlterFehler und die Funktion lies_alter(text).

  • AlterFehler ist eine Art von ValueError. Sie wird mit AlterFehler(wert, grund) erzeugt, speichert beide Werte als Attribute wert und grund, und str(e) ist f"{grund}: {wert!r}".
  • lies_alter wandelt text in eine ganze Zahl um und gibt sie zurück. Leerzeichen am Rand (" 7 ") verarbeitet int selbst, du musst nichts abschneiden.
  • Ist der Text keine ganze Zahl, löst sie AlterFehler(text, "keine Zahl") aus, mit dem ursprünglichen Fehler als Ursache (__cause__).
  • Liegt die Zahl nicht zwischen 0 und 150 (beide inklusive), löst sie AlterFehler(text, "ausserhalb 0 bis 150") aus.
  • Andere Fehler (zum Beispiel None als Eingabe) sollen nicht umgewandelt werden, sondern unverändert nach oben gehen.

Die letzte Zeile gibt beides als Tupel zurück.

Für __init__ hilft Schritt 4. Für die Ursache brauchst du das from aus Schritt 3. Teste im Kopf: Was passiert mit deinem eigenen Fehler, wenn er innerhalb des try-Blocks ausgelöst wird, dessen except du gerade geschrieben hast?

class AlterFehler(ValueError):
    def __init__(self, wert, grund):
        super().__init__(f"{grund}: {wert!r}")
        self.wert = wert
        self.grund = grund

def lies_alter(text):
    try:
        alter = int(text)
    except ValueError as alt:
        raise AlterFehler(text, "keine Zahl") from alt
    if not 0 <= alter <= 150:
        raise AlterFehler(text, "ausserhalb 0 bis 150")
    return alter

(AlterFehler, lies_alter)

Übung 4: Fehler finden

Diese Funktion soll den Mittelwert einer Gruppe liefern. Sie hat mehrere Fehler. Soll-Verhalten:

  • Gruppe unbekannt (KeyError): Rückgabe "unbekannt".
  • Gruppe leer (ZeroDivisionError): Rückgabe 0.0.
  • Jeder andere Fehler (zum Beispiel ein String in der Liste, TypeError) geht nach oben.
  • Bei jedem Aufruf, auch bei einem Fehler, kommt der Gruppenname genau einmal in protokoll.

Repariere die Funktion.

Spiele für jeden der vier Soll-Punkte einen Aufruf im Kopf durch und notiere, was der Code zurückgibt. Wo weicht das Ergebnis ab, und welche Zeile ist dafür verantwortlich? Es steckt mehr als ein Fehler drin.

def mittelwert(daten, gruppe, protokoll):
    try:
        werte = daten[gruppe]
        return sum(werte) / len(werte)
    except KeyError:
        return "unbekannt"
    except ZeroDivisionError:
        return 0.0
    finally:
        protokoll.append(gruppe)

mittelwert

Übung 5: Wiederholen bei Fehlern (Retry)

Schreibe wiederhole(funktion, versuche, erlaubt). Sie ruft funktion() ohne Argumente auf, höchstens versuche Mal (mindestens 1).

  • Klappt ein Aufruf, wird dessen Rückgabewert zurückgegeben (auch None oder 0).
  • Löst funktion eine Exception aus, die zum Tupel erlaubt passt (auch Unterklassen), wird erneut versucht.
  • Ist der letzte Versuch gescheitert, geht dieselbe Exception unverändert nach oben.
  • Passt die Exception nicht zu erlaubt, geht sie sofort nach oben, ohne weiteren Versuch.

Wo entscheidest du, ob ein Fehler “der letzte” war? Und welche Schreibweise von raise behält genau das ursprüngliche Objekt? Was ist der Unterschied zwischen except erlaubt: und except Exception:?

def wiederhole(funktion, versuche, erlaubt):
    for versuch in range(1, versuche + 1):
        try:
            return funktion()
        except erlaubt:
            if versuch == versuche:
                raise

wiederhole

Merksatz und Prüfstein

Merksatz: Der erste passende except gewinnt (spezifisch vor allgemein), else läuft nur ohne Fehler, finally läuft immer (und ein return darin überschreibt alles), und raise allein reicht das Original weiter.

Prüfstein (offene Frage): Du schreibst try: ... except Exception: pass um einen ganzen Funktionsaufruf. Welche drei Dinge können dadurch unbemerkt schiefgehen, und wie würdest du es mit Typen, else und raise besser machen?

Quelle: quellen/python-glossar-pcap-pcpp1.md, Abschnitt “Exceptions” (Ablauf, Hierarchie, Typische Auslöser, Prüfungsfallen) und Prüfungsübersicht (Block 2: 5 Fragen, 14 %). Hinweis: Die Aussagen zu __cause__ und raise Neu from Alt stehen in der Quelle im PCPP1-Teil (1.9 Erweiterte Exceptions), der Fall raise "text" ergibt TypeError und der RuntimeError für raise ohne aktive Exception stehen nicht in der Quelle, wurden aber mit Python 3.13 ausgeführt (bitte prüfen, ob sie in der PCAP geprüft werden).