Fehler gehoeren zum Programmieren dazu. Entscheidend ist, wie du mit ihnen umgehst. Python bietet ein maaechtiges Exception-System, das weit ueber ein einfaches try/except hinausgeht. In diesem Artikel lernst du sieben Muster, die deinen Code robuster und wartbarer machen.
1. try/except – Die Grundlage
Das Fundament der Fehlerbehandlung in Python. Fange immer spezifische Exceptions, nie ein nacktes except:.
Schlecht: Alles abfangen
# Nicht empfohlen!
try:
result = int(user_input)
except:
print("Fehler")
Besser: Spezifische Exception
try:
result = int("abc")
except ValueError as e:
print(f"Ungueltige Eingabe: {e}")
except TypeError as e:
print(f"Falscher Typ: {e}")
Warum spezifisch? Ein nacktes except faengt auch KeyboardInterrupt und SystemExit ab – das macht Debugging zum Albtraum und verhindert, dass du dein Programm sauber beenden kannst.
2. Der vollstaendige Block: try/except/else/finally
Python bietet vier Bloecke fuer die Fehlerbehandlung. else wird nur ausgefuehrt, wenn keine Exception aufgetreten ist. finally laeuft immer:
import json
def load_config(path):
try:
f = open(path, "r")
except FileNotFoundError:
print(f"Datei nicht gefunden: {path}")
return {}
else:
try:
data = json.load(f)
except json.JSONDecodeError as e:
print(f"JSON-Fehler: {e}")
data = {}
return data
finally:
try:
f.close()
except NameError:
pass # f wurde nie erzeugt
config = load_config("settings.json")
print(config)
Die Aufteilung ist klar: try fuer den riskanten Code, except fuer die Fehlerbehandlung, else fuer den Erfolgsfall und finally fuer Aufraeumarbeiten.
3. Context Manager – Ressourcen sicher verwalten
Der with-Block garantiert, dass Ressourcen freigegeben werden – auch bei Exceptions:
# Datei wird automatisch geschlossen
with open("daten.txt", "r") as f:
inhalt = f.read()
# Eigener Context Manager
from contextlib import contextmanager
import time
@contextmanager
def timer(label):
start = time.perf_counter()
try:
yield
finally:
elapsed = time.perf_counter() - start
print(f"{label}: {elapsed:.3f}s")
with timer("Berechnung"):
total = sum(range(10_000_000))
print(f"Ergebnis: {total}")
Context Manager sind besonders wichtig fuer Dateien, Datenbankverbindungen, Locks und Netzwerk-Sockets. Der finally-Block im Generator stellt sicher, dass die Zeitmessung auch bei Fehlern ausgegeben wird.
4. raise – Exceptions bewusst ausloesen
Manchmal musst du selbst Exceptions ausloesen. Mit raise ... from ... kannst du Exception-Ketten bilden:
def teile(a, b):
if not isinstance(b, (int, float)):
raise TypeError(f"b muss eine Zahl sein, nicht {type(b).__name__}")
if b == 0:
raise ValueError("Division durch Null ist nicht erlaubt")
return a / b
# Exception-Ketten
def parse_config(raw):
try:
import json
return json.loads(raw)
except json.JSONDecodeError as e:
raise ValueError("Ungueltige Konfiguration") from e
try:
parse_config("{invalid")
except ValueError as e:
print(f"Fehler: {e}")
print(f"Ursache: {e.__cause__}")
from e haengt die urspruengliche Exception als __cause__ an. So bleiben Fehlerketten nachvollziehbar, auch wenn du die Exception in eine verstaendlichere umwandelst.
5. Eigene Exception-Klassen
Fuer groessere Projekte solltest du eigene Exception-Hierarchien definieren. Das macht den Code ausdrueckstaerker:
class AppError(Exception):
"""Basis-Exception fuer die Anwendung."""
pass
class ValidationError(AppError):
def __init__(self, field, message):
self.field = field
self.message = message
super().__init__(f"{field}: {message}")
class NotFoundError(AppError):
def __init__(self, resource, id_):
self.resource = resource
self.id_ = id_
super().__init__(f"{resource} mit ID {id_} nicht gefunden")
# Verwendung
def get_user(user_id):
if user_id < 0:
raise ValidationError("user_id", "Muss positiv sein")
# Simuliere Datenbank-Abfrage
raise NotFoundError("User", user_id)
try:
get_user(42)
except NotFoundError as e:
print(f"Nicht gefunden: {e.resource} #{e.id_}")
except ValidationError as e:
print(f"Validierungsfehler: {e.field} - {e.message}")
except AppError as e:
print(f"Allgemeiner Fehler: {e}")
Die Hierarchie erlaubt es, spezifische Fehler gezielt zu fangen oder mit except AppError alle anwendungsspezifischen Fehler auf einmal zu behandeln.
6. assert – Invarianten pruefen
assert ist kein Ersatz fuer Fehlerbehandlung, sondern ein Werkzeug fuer Entwickler, um Annahmen zu dokumentieren und Programmierfehler frueh zu finden:
def berechne_rabatt(preis, prozent):
assert 0 <= prozent <= 100, f"Prozent muss 0-100 sein, war {prozent}"
assert preis >= 0, "Preis darf nicht negativ sein"
return preis * (1 - prozent / 100)
# Funktioniert
print(berechne_rabatt(100, 20)) # 80.0
# Schlaegt fehl (nur im Debug-Modus)
# berechne_rabatt(100, 150) # AssertionError
Wichtig: assert-Anweisungen werden entfernt, wenn Python mit -O (Optimierung) gestartet wird. Verwende sie daher nie fuer Eingabevalidierung – nur fuer interne Konsistenzpruefungen.
Wann assert, wann raise?
assert: Interne Annahmen, die bei korrektem Code nie fehlschlagen sollten. raise: Erwartbare Fehler durch externe Eingaben (Benutzerdaten, API-Antworten, Dateien).
7. Retry-Logik – Transiente Fehler abfangen
Netzwerkfehler, Timeouts, Datenbank-Locks – manche Fehler verschwinden beim zweiten Versuch. Ein Retry-Dekorator loest das elegant:
import time
import functools
def retry(max_attempts=3, delay=1.0, backoff=2.0, exceptions=(Exception,)):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
attempt = 0
current_delay = delay
while True:
try:
return func(*args, **kwargs)
except exceptions as e:
attempt += 1
if attempt >= max_attempts:
raise
print(f"Versuch {attempt}/{max_attempts} fehlgeschlagen: {e}")
print(f"Naechster Versuch in {current_delay:.1f}s ...")
time.sleep(current_delay)
current_delay *= backoff
return wrapper
return decorator
# Anwendung
@retry(max_attempts=3, delay=0.5, exceptions=(ConnectionError, TimeoutError))
def fetch_data(url):
import urllib.request
with urllib.request.urlopen(url, timeout=5) as resp:
return resp.read()
# Exponentielles Backoff: 0.5s, 1.0s, 2.0s ...
Der Dekorator unterstuetzt exponentielles Backoff: Die Wartezeit verdoppelt sich nach jedem Fehlversuch. Das verhindert, dass du einen ohnehin ueberlasteten Server noch weiter belastest.
Retry mit Context Manager
import time
from contextlib import contextmanager
@contextmanager
def retry_block(max_attempts=3, delay=1.0, exceptions=(Exception,)):
for attempt in range(1, max_attempts + 1):
try:
yield attempt
return # Erfolg
except exceptions as e:
if attempt == max_attempts:
raise
print(f"Versuch {attempt} fehlgeschlagen: {e}")
time.sleep(delay)
# Verwendung
with retry_block(max_attempts=3, exceptions=(IOError,)) as attempt:
print(f"Versuch #{attempt}")
data = open("wichtige_datei.txt").read()
Fazit
Gute Fehlerbehandlung ist kein Luxus, sondern eine Notwendigkeit fuer produktionsreifen Code. Fasse die wichtigsten Regeln zusammen:
1. Fange immer spezifische Exceptions.
2. Nutze finally und Context Manager fuer Aufraeumarbeiten.
3. Erstelle eigene Exception-Klassen fuer groessere Projekte.
4. Verwende assert nur fuer interne Invarianten.
5. Implementiere Retry-Logik fuer transiente Fehler.
6. Erhalte Exception-Ketten mit raise ... from ....
Mit diesen sieben Mustern bist du fuer die meisten Fehlerszenarien gewappnet. Dein Code wird stabiler, deine Fehlermeldungen hilfreicher und dein Debugging deutlich schneller.

Schreibe einen Kommentar