7 Python-Fehlerbehandlungsmuster, die jeder Entwickler kennen sollte

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

Python
# Nicht empfohlen!
try:
    result = int(user_input)
except:
    print("Fehler")

Besser: Spezifische Exception

Python
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:

try_complete.py
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:

context_manager.py
# 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:

raise_chain.py
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:

custom_exceptions.py
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:

assert_example.py
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:

retry_decorator.py
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

retry_context.py
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.

Comments

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert