- 1 Section
- 10 Lessons
- unbegrenzt
- Python Fortgeschritten10
- 1.1Module und Pakete: import, pip, venv
- 1.2Objektorientierung in Python: Klassen, __init__, self
- 1.3Vererbung und Mehrfachvererbung in Python
- 1.4Decorators und Generatoren
- 1.5Fehlerbehandlung vertieft: eigene Exceptions
- 1.6Reguläre Ausdrücke mit re
- 1.7Arbeiten mit APIs: requests, JSON
- 1.8Automatisierung mit os, subprocess, pathlib
- 1.9Unit-Tests in Python: pytest
- 1.10Praxisprojekt: Automatisierungsskript mit Tests
Fehlerbehandlung vertieft: eigene Exceptions
In Python Grundlagen hast du try und except kennengelernt – die Basis, mit der ein Programm Fehler abfängt statt abzustürzen. Diese Lektion geht tiefer: die Exception-Hierarchie, die volle try/except/else/finally-Struktur, eigene Exception-Klassen, das Werfen mit Kontext (raise from), Exception Groups (Python 3.11+) und das wichtige Idiom „EAFP" – „Easier to Ask Forgiveness than Permission".
Eine gute Fehlerbehandlung unterscheidet hobbymäßigen von professionellem Code. Stürzt dein Programm wegen jeder Kleinigkeit ab? Oder erkennt es Probleme, gibt klare Meldungen und versucht eine Wiederherstellung? Genau das ist Ziel dieser Lektion.
1) Wozu Exceptions?
Eine Exception (deutsch: Ausnahme) signalisiert, dass etwas Ungewöhnliches passiert ist – eine Division durch Null, eine fehlende Datei, ein ungültiges Format. Statt einen Fehlercode zurückzugeben wie in C, bricht Python die normale Programmausführung ab und „wirft" die Exception nach oben durch alle Funktionsaufrufe, bis jemand sie fängt – oder bis das Programm abstürzt.
x = int("abc") # ValueError: invalid literal for int() with base 10: 'abc' datei = open("nicht_da.txt") # FileNotFoundError: [Errno 2] No such file or directory [1, 2, 3][10] # IndexError: list index out of range
Jede dieser Fehlersituationen erzeugt eine Exception eines bestimmten Typs. Das ermöglicht, gezielt auf bestimmte Fehler zu reagieren.
2) Die Exception-Hierarchie
Alle Exceptions sind Klassen, die voneinander erben. An der Spitze steht BaseException. Fast alle „normalen" Fehler erben von Exception – einer Tochter von BaseException. Die Hierarchie ist wichtig: wenn du except Exception: schreibst, fängst du auch alle Tochter-Klassen wie ValueError, FileNotFoundError usw.
SystemExit und KeyboardInterrupt erben direkt von BaseException, nicht von Exception. Dadurch fängst du sie nicht mit except Exception:. Das ist Absicht – Strg+C soll dein Programm beenden können.3) try/except/else/finally
Die vollständige Struktur einer Fehlerbehandlung in Python hat vier Teile:
try: f = open("daten.txt") inhalt = f.read() except FileNotFoundError: print("Datei fehlt") except PermissionError: print("Keine Berechtigung") else: print(f"Inhalt: {inhalt}") # läuft nur, wenn KEIN Fehler kam finally: try: f.close() # läuft IMMER, mit oder ohne Fehler except NameError: pass
except E as e: bindet die Exception an die Variable e.return kam. Typisch für Cleanup wie Datei schließen.In der Praxis kommt else seltener vor – meist erledigt der erfolgreiche Pfad seine Arbeit direkt im try-Block. finally ist wichtig für Aufräum-Arbeiten; oft kann es aber durch with (Context Manager, siehe Decorators und Generatoren) ersetzt werden.
4) Mehrere Exceptions in einer except-Zeile
Wenn mehrere Fehlerarten gleich behandelt werden sollen, schreibt man sie als Tupel:
try: daten = json.loads(text) except (ValueError, TypeError) as e: print(f"Daten nicht lesbar: {e}")
Wichtig: das ist ein Tupel in Klammern. except ValueError, TypeError: (Komma statt Klammer) ist Python-2-Syntax und gibt heute einen Fehler.
5) Die Exception-Information nutzen
Mit as bindest du die Exception-Instanz an eine Variable. So bekommst du Zugriff auf die Fehlermeldung:
try: x = int("abc") except ValueError as e: print(f"Fehler: {e}") print(f"Typ: {type(e).__name__}") print(f"Args: {e.args}") # Fehler: invalid literal for int() with base 10: 'abc' # Typ: ValueError # Args: ("invalid literal for int() with base 10: 'abc'",)
6) Wichtige eingebaute Exceptions
int("abc"), list.remove(x) mit fehlendem x."a" + 1, len(5).{"a":1}["b"].[1,2][5].None.upper().open("...").7) Eigene Exceptions definieren
Eigene Exception-Klassen sind sinnvoll, wenn dein Modul oder deine Bibliothek spezifische Fehler hat, die aufrufender Code gezielt fangen will. Sie erben von Exception (oder einer passenden Tochter):
class DatenbankFehler(Exception): """Allgemeiner Fehler beim Datenbank-Zugriff.""" class VerbindungVerloren(DatenbankFehler): """Verbindung zur DB wurde getrennt.""" class UngueltigeAbfrage(DatenbankFehler): """SQL-Syntax ist ungültig.""" def query(sql): if not sql.strip(): raise UngueltigeAbfrage("Leere SQL-Abfrage") # ...
Aufrufender Code kann jetzt entweder spezifisch UngueltigeAbfrage oder generell DatenbankFehler fangen:
try: query("") except UngueltigeAbfrage as e: print(f"Syntax-Problem: {e}") except DatenbankFehler as e: print(f"Allgemeines DB-Problem: {e}")
8) raise – Exceptions selbst werfen
Mit raise wirfst du eine Exception. Sie ist ein normales Objekt mit Konstruktor:
def teile(a, b): if b == 0: raise ValueError("Division durch Null nicht erlaubt") return a / b try: teile(10, 0) except ValueError as e: print(e) # Division durch Null nicht erlaubt
Innerhalb eines except-Blocks kannst du mit raise (ohne Argument) die aktuelle Exception einfach weiterreichen, etwa nach dem Loggen:
try: verarbeite() except Exception as e: log.error(f"Fehler in verarbeite: {e}") raise # reicht den Fehler weiter, bewahrt den Stacktrace
9) raise from – Kontext mitgeben
Wenn eine Exception eine andere auslöst, kannst du beide verknüpfen. Das macht Debugging viel einfacher:
def benutzer_laden(id): try: return db.query(f"SELECT * FROM users WHERE id={id}") except DatenbankFehler as e: raise BenutzerNichtGefunden(f"ID {id} nicht ladbar") from e
Der Aufrufer sieht jetzt: „BenutzerNichtGefunden, ausgelöst durch DatenbankFehler". Im Traceback steht: „The above exception was the direct cause of the following exception." So weiß man, was wirklich passiert ist, ohne den ursprünglichen Fehler zu verlieren.
Mit raise NeueException(...) from None kannst du den ursprünglichen Fehler explizit verbergen – falls der nichts hilfreiches liefert.
10) EAFP – Easier to Ask Forgiveness than Permission
Python hat ein eigenes Idiom für Fehlerbehandlung. Statt erst zu prüfen, ob etwas geht (LBYL – „Look Before You Leap"), versuchst du es einfach und fängst die Exception (EAFP):
# LBYL – Stil aus anderen Sprachen if "key" in daten: wert = daten["key"] else: wert = None # EAFP – pythonisch try: wert = daten["key"] except KeyError: wert = None # Oder noch kürzer mit dict.get(): wert = daten.get("key")
EAFP ist nicht nur kürzer, sondern auch frei von Race Conditions: zwischen dem if "key" in daten und dem späteren Zugriff könnte sich das Dict in einer parallelen Welt ändern. EAFP atomar.
Trotzdem nicht jeder Fehler verdient eine Exception – wenn ein Wert „normalerweise fehlt", ist dict.get() oder ein vorab-Check oft klarer.
11) Exception Groups (Python 3.11+)
Seit Python 3.11 gibt es ExceptionGroup – sammelt mehrere Exceptions und wirft sie zusammen. Hilfreich bei parallelen Aufgaben, wo mehrere Fehler gleichzeitig passieren können:
try: raise ExceptionGroup("Mehrere Probleme", [ ValueError("falscher Wert"), KeyError("fehlt was"), ]) except* ValueError as e: print("Wert-Fehler", e.exceptions) except* KeyError as e: print("Schlüssel-Fehler", e.exceptions)
Beachte das except* mit Stern: damit fängst du gezielt Exceptions aus einer Group. Selten benötigt, aber gut zu wissen.
12) Was nicht zu tun ist
try: irgendwas() except: pass # schlechtester Code-Stil aller ZeitenDamit verbirgst du jeden Fehler – auch
KeyboardInterrupt. Bugs werden unsichtbar, dein Programm hängt geheimnisvoll. Niemals!
Ein paar weitere Anti-Patterns:
except Exception:zu früh: fängt alles ab. Nur sinnvoll am äußersten Rand (z. B. Main-Loop oder Logging), nicht in Library-Code- Exception nur fangen, um neu zu werfen: ohne neue Info ist es sinnlos. Lass sie hochsprudeln
- Exceptions für normale Kontrolle:
try: f(x) except: passals Ersatz für eine if-Bedingung verwirrt Leser - Exception-Klassen ohne Doku: eine eigene Exception ohne Docstring lässt offen, wann sie geworfen wird
- Generische Fehlermeldungen:
raise Exception("Fehler")– wenig hilfreich. Lieber spezifische Typen mit aussagekräftiger Message
13) Logging statt print
Im echten Code sollte ein gefangener Fehler nicht mit print ausgegeben werden, sondern mit dem Standard-Modul logging. Es bietet Levels (DEBUG, INFO, WARNING, ERROR, CRITICAL), Ausgabe-Ziele und automatisches Format:
import logging logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") log = logging.getLogger(__name__) try: verarbeite() except ValueError as e: log.warning(f"Wert ungültig: {e}") except Exception: log.exception("Unerwarteter Fehler") # inkl. Stacktrace raise
log.exception() ist besonders praktisch – es loggt nicht nur die Nachricht, sondern auch den ganzen Stacktrace. Mehr zu Logging in Debugging & Fehleranalyse.
14) Stacktrace verstehen
Wenn eine Exception nicht gefangen wird, gibt Python einen Traceback aus – die Liste der Funktionsaufrufe, die zum Fehler führten. Lies ihn von unten nach oben:
Traceback (most recent call last): File "app.py", line 25, in <module> main() File "app.py", line 15, in main verarbeite(daten) File "app.py", line 8, in verarbeite return daten["key"] / 0 ZeroDivisionError: division by zero
Unten steht der konkrete Fehler – hier ZeroDivisionError. Darüber: die Funktion, in der er passierte (verarbeite, Zeile 8), und der Weg dorthin (main rief verarbeite, Modul-Start rief main). Wer Tracebacks lesen kann, debuggt deutlich schneller.
15) Häufige Fehler
- Bare
except:– fängt auchSystemExitundKeyboardInterrupt. Immer mit konkretem Typ schreiben - Reihenfolge der except-Blöcke falsch:
except Exception:vorexcept ValueError:– der zweite wird nie erreicht. Spezifische zuerst! - Exception schlucken statt loggen:
except: passversteckt Probleme. Mindestens loggen - Exception in
finallywerfen: ersetzt die ursprüngliche Exception, geht oft verloren raise estattraise: zerstört den Original-Stacktrace. Innerhalb von except: einfachraisenutzen- Exception-Konstruktor falsch aufgerufen:
raise ValueError, "Text"ist Python-2-Syntax, heute ungültig - Eigene Exception von
BaseExceptionerben lassen: NIE. Immer vonExceptionoder Tocher
except: pass ist immer falsch.
Zusammenfassung
Exceptions sind Klassen, die in einer Hierarchie organisiert sind: BaseException → Exception → speziellere Typen wie ValueError oder FileNotFoundError. Mit try/except/else/finally behandelst du Fehler: except fängt einen oder mehrere Typen, else läuft nur bei Erfolg, finally immer. Eigene Exception-Klassen erben von Exception; sie machen Bibliotheks-Code testbar und kommunikativ. Werfen mit raise Typ("Nachricht"); mit raise X from Y verknüpfst du Kontext. EAFP („Easier to Ask Forgiveness than Permission") ist der pythonische Stil – einfach versuchen und Fehler fangen, statt vorab zu prüfen. Niemals bare except: oder except: pass. Mit logging statt print protokollieren. Tracebacks von unten nach oben lesen.
