Zum Inhalt springen
Python Fortgeschritten 2 min

Eigene Exceptions in Python erstellen

Lerne, wie du eigene Exception-Klassen erstellst, um aussagekräftige Fehlermeldungen in deinen Python-Programmen zu erzeugen.

Aktualisiert:

Eigene Exceptions in Python erstellen

Die eingebauten Exceptions wie ValueError oder TypeError decken viele Fälle ab — aber manchmal brauchst du eigene Fehlertypen, die genau zu deiner Anwendung passen. In diesem Tutorial lernst du, wie du professionelle Exception-Klassen erstellst.

Warum eigene Exceptions?

Stell dir vor, du baust ein Benutzerverwaltungssystem. Was passiert bei ungültigem Passwort?

# Unpräzise - was genau ist "falsch"?
raise ValueError("Ungültiges Passwort")

# Viel besser - sofort klar, was passiert ist!
raise PasswortZuKurzError("Passwort muss mindestens 8 Zeichen haben")

Eigene Exceptions bieten dir:

  • Klarheit: Der Fehlertyp sagt sofort, was schiefgelaufen ist
  • Gezielte Behandlung: Du kannst verschiedene Fehler unterschiedlich abfangen
  • Zusätzliche Informationen: Du kannst eigene Attribute mitgeben
  • Professionelle APIs: Nutzer deines Codes können gezielt auf bestimmte Fehler reagieren

Die raise-Anweisung

Bevor wir eigene Exceptions erstellen, schauen wir uns raise an. Damit löst du einen Fehler manuell aus:

def alter_setzen(alter):
    if not isinstance(alter, int):
        raise TypeError("Alter muss eine Ganzzahl sein!")
    if alter < 0:
        raise ValueError("Alter darf nicht negativ sein!")
    if alter > 150:
        raise ValueError("Alter scheint unrealistisch!")
    return alter

# Testen
try:
    alter_setzen(-5)
except ValueError as e:
    print(f"Fehler: {e}")
    # Ausgabe: Fehler: Alter darf nicht negativ sein!

Du kannst auch einen abgefangenen Fehler weitergeben:

try:
    zahl = int(eingabe)
except ValueError:
    print("Fehler wurde protokolliert.")
    raise  # Fehler wird erneut ausgelöst

Oder einen Fehler mit einer Ursache verknüpfen:

try:
    wert = int(text)
except ValueError as original:
    raise EingabeFehler(f"'{text}' ist keine Zahl") from original

Exception-Klassen erstellen

Eine eigene Exception ist einfach eine Klasse, die von Exception erbt:

class MeinFehler(Exception):
    """Ein benutzerdefinierter Fehler."""
    pass

# Verwenden
raise MeinFehler("Etwas ist schiefgelaufen!")

Das war’s schon für den einfachsten Fall! Aber meistens möchtest du mehr:

class AlterUngueltigError(Exception):
    """Wird ausgelöst, wenn ein ungültiges Alter angegeben wird."""
    pass

class NameLeerError(Exception):
    """Wird ausgelöst, wenn ein leerer Name angegeben wird."""
    pass

def person_erstellen(name, alter):
    if not name or not name.strip():
        raise NameLeerError("Der Name darf nicht leer sein!")
    if not isinstance(alter, int) or alter < 0:
        raise AlterUngueltigError(f"Ungültiges Alter: {alter}")

    return {"name": name.strip(), "alter": alter}

# Verwendung
try:
    person = person_erstellen("", 25)
except NameLeerError as e:
    print(f"Namensfehler: {e}")
except AlterUngueltigError as e:
    print(f"Altersfehler: {e}")

Eigene Fehlermeldungen

Du kannst den Konstruktor überschreiben, um Standardmeldungen zu definieren:

class DatenbankVerbindungsFehler(Exception):
    """Fehler bei der Datenbankverbindung."""

    def __init__(self, host="localhost", port=5432, nachricht=None):
        self.host = host
        self.port = port
        if nachricht is None:
            nachricht = f"Verbindung zu {host}:{port} fehlgeschlagen"
        super().__init__(nachricht)

# Verwenden
raise DatenbankVerbindungsFehler()
# DatenbankVerbindungsFehler: Verbindung zu localhost:5432 fehlgeschlagen

raise DatenbankVerbindungsFehler("db.example.com", 3306)
# DatenbankVerbindungsFehler: Verbindung zu db.example.com:3306 fehlgeschlagen

raise DatenbankVerbindungsFehler(
    nachricht="Zeitüberschreitung beim Verbindungsaufbau"
)
# DatenbankVerbindungsFehler: Zeitüberschreitung beim Verbindungsaufbau

Exception-Hierarchie verstehen

Pythons eingebaute Exceptions bilden eine Hierarchie:

BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
└── Exception
    ├── ValueError
    ├── TypeError
    ├── KeyError
    ├── IndexError
    ├── FileNotFoundError (erbt von OSError)
    ├── AttributeError
    └── ... viele weitere

Wichtig: except Exception fängt alles unter Exception ab, aber nicht SystemExit oder KeyboardInterrupt. Das ist gewollt!

try:
    # Code
    pass
except Exception:
    # Fängt ValueError, TypeError, KeyError usw. ab
    # Fängt NICHT KeyboardInterrupt oder SystemExit ab
    pass

Eigene Exception-Hierarchie erstellen

Für größere Projekte erstellst du eine eigene Hierarchie:

# Basis-Exception für die gesamte Anwendung
class AppError(Exception):
    """Basis-Exception für unsere Anwendung."""
    pass

# Benutzer-bezogene Fehler
class BenutzerError(AppError):
    """Basis für alle benutzerbezogenen Fehler."""
    pass

class BenutzerNichtGefundenError(BenutzerError):
    """Benutzer wurde nicht in der Datenbank gefunden."""
    pass

class AnmeldungFehlgeschlagenError(BenutzerError):
    """Anmeldedaten sind ungültig."""
    pass

class BerechtigungsFehler(BenutzerError):
    """Benutzer hat nicht die nötige Berechtigung."""
    pass

# Daten-bezogene Fehler
class DatenError(AppError):
    """Basis für alle datenbezogenen Fehler."""
    pass

class ValidierungsFehler(DatenError):
    """Eingabedaten sind ungültig."""
    pass

class DatenbankError(DatenError):
    """Fehler bei Datenbankoperationen."""
    pass

Jetzt kannst du auf verschiedenen Ebenen abfangen:

try:
    benutzer_anmelden(name, passwort)
except AnmeldungFehlgeschlagenError:
    # Nur Anmeldungsfehler
    print("Falsche Anmeldedaten!")
except BenutzerError:
    # Alle benutzerbezogenen Fehler
    print("Benutzerfehler aufgetreten.")
except AppError:
    # Alle App-Fehler
    print("Ein Anwendungsfehler ist aufgetreten.")

Exception mit zusätzlichen Attributen

Eigene Exceptions können beliebige Zusatzinformationen tragen:

class ValidierungsFehler(Exception):
    """Fehler bei der Datenvalidierung."""

    def __init__(self, feld, wert, nachricht=None):
        self.feld = feld
        self.wert = wert
        self.nachricht = nachricht or f"Ungültiger Wert für '{feld}': {wert}"
        super().__init__(self.nachricht)


class MehrfachValidierungsFehler(Exception):
    """Mehrere Validierungsfehler gleichzeitig."""

    def __init__(self, fehler_liste):
        self.fehler = fehler_liste
        nachrichten = [f.nachricht for f in fehler_liste]
        super().__init__(
            f"{len(fehler_liste)} Validierungsfehler:\n"
            + "\n".join(f"  - {n}" for n in nachrichten)
        )


def benutzer_validieren(daten):
    fehler = []

    if not daten.get("name"):
        fehler.append(
            ValidierungsFehler("name", daten.get("name"), "Name ist erforderlich")
        )

    email = daten.get("email", "")
    if "@" not in email:
        fehler.append(
            ValidierungsFehler("email", email, "Ungültige E-Mail-Adresse")
        )

    alter = daten.get("alter", 0)
    if not (0 < alter < 150):
        fehler.append(
            ValidierungsFehler("alter", alter, "Alter muss zwischen 1 und 149 liegen")
        )

    if fehler:
        raise MehrfachValidierungsFehler(fehler)

    return True


# Verwendung
try:
    benutzer_validieren({"name": "", "email": "ungueltig", "alter": -5})
except MehrfachValidierungsFehler as e:
    print(e)
    # 3 Validierungsfehler:
    #   - Name ist erforderlich
    #   - Ungültige E-Mail-Adresse
    #   - Alter muss zwischen 1 und 149 liegen

    # Auf einzelne Fehler zugreifen
    for fehler in e.fehler:
        print(f"  Feld: {fehler.feld}, Wert: {fehler.wert}")

__str__ für aussagekräftige Fehlermeldungen

Du kannst die Darstellung deiner Exception anpassen:

class HTTPError(Exception):
    """HTTP-Fehler mit Statuscode."""

    CODES = {
        400: "Bad Request",
        401: "Unauthorized",
        403: "Forbidden",
        404: "Not Found",
        500: "Internal Server Error",
    }

    def __init__(self, status_code, url=None, detail=None):
        self.status_code = status_code
        self.url = url
        self.detail = detail
        super().__init__(str(self))

    def __str__(self):
        code_text = self.CODES.get(self.status_code, "Unknown Error")
        teile = [f"HTTP {self.status_code} {code_text}"]
        if self.url:
            teile.append(f"URL: {self.url}")
        if self.detail:
            teile.append(f"Detail: {self.detail}")
        return " | ".join(teile)

    def __repr__(self):
        return (
            f"HTTPError(status_code={self.status_code}, "
            f"url={self.url!r}, detail={self.detail!r})"
        )


# Verwenden
try:
    raise HTTPError(404, url="/api/benutzer/42", detail="Benutzer nicht gefunden")
except HTTPError as e:
    print(e)
    # HTTP 404 Not Found | URL: /api/benutzer/42 | Detail: Benutzer nicht gefunden
    print(f"Statuscode: {e.status_code}")
    # Statuscode: 404

Best Practices

Wann eigene Exceptions erstellen?

# JA - wenn deine Anwendung spezifische Fehlertypen hat
class KontoUeberzeichnetError(Exception):
    """Kontostand reicht für die Transaktion nicht aus."""
    pass

# JA - wenn Aufrufer unterschiedlich reagieren sollen
class ArtikelAusverkauftError(Exception):
    pass

class WarenkorbLeerError(Exception):
    pass

# NEIN - wenn ein eingebauter Typ passt
# Verwende einfach ValueError:
def positiv(n):
    if n < 0:
        raise ValueError(f"Zahl muss positiv sein, war aber {n}")
    return n

Namenskonvention

Exception-Klassen enden immer auf Error:

# Gut
class ValidierungsFehler(Exception): ...
class VerbindungsError(Exception): ...
class BerechtigungsError(Exception): ...

# Schlecht
class UngueltigeEingabe(Exception): ...    # Kein "Error" am Ende
class VALIDIERUNG_FEHLER(Exception): ...   # Falsche Namenskonvention

Hierarchie richtig aufbauen

# Immer von Exception erben, NICHT von BaseException
class MeinFehler(Exception):     # Richtig
    pass

class MeinFehler(BaseException): # Falsch -- nur für Spezialfälle
    pass

Dokumentation nicht vergessen

class TransaktionsFehler(Exception):
    """Wird ausgelöst, wenn eine Banktransaktion fehlschlägt.

    Attribute:
        betrag: Der versuchte Transaktionsbetrag
        kontostand: Der aktuelle Kontostand
        transaktion_id: Eindeutige ID der fehlgeschlagenen Transaktion
    """

    def __init__(self, betrag, kontostand, transaktion_id=None):
        self.betrag = betrag
        self.kontostand = kontostand
        self.transaktion_id = transaktion_id
        super().__init__(
            f"Transaktion über {betrag:.2f} EUR fehlgeschlagen. "
            f"Kontostand: {kontostand:.2f} EUR"
        )

Praxis: Validierungs-Exceptions für ein Registrierungsformular

Hier ist ein vollständiges Beispiel für ein Benutzer-Registrierungssystem:

import re
from datetime import date


# --- Exception-Hierarchie ---

class RegistrierungsFehler(Exception):
    """Basis-Exception für Registrierungsfehler."""
    pass


class BenutzernameError(RegistrierungsFehler):
    """Fehler beim Benutzernamen."""
    pass


class BenutzernameZuKurzError(BenutzernameError):
    """Benutzername hat zu wenige Zeichen."""
    def __init__(self, name, min_laenge=3):
        self.name = name
        self.min_laenge = min_laenge
        super().__init__(
            f"Benutzername '{name}' ist zu kurz "
            f"(mindestens {min_laenge} Zeichen erforderlich)"
        )


class BenutzernameVergebenError(BenutzernameError):
    """Benutzername ist bereits vergeben."""
    def __init__(self, name):
        self.name = name
        super().__init__(f"Benutzername '{name}' ist bereits vergeben")


class PasswortError(RegistrierungsFehler):
    """Fehler beim Passwort."""
    pass


class PasswortZuKurzError(PasswortError):
    def __init__(self, laenge, min_laenge=8):
        self.laenge = laenge
        self.min_laenge = min_laenge
        super().__init__(
            f"Passwort hat {laenge} Zeichen "
            f"(mindestens {min_laenge} erforderlich)"
        )


class PasswortZuSchwachError(PasswortError):
    def __init__(self, fehlende_kriterien):
        self.fehlende_kriterien = fehlende_kriterien
        kriterien_text = ", ".join(fehlende_kriterien)
        super().__init__(
            f"Passwort erfüllt folgende Kriterien nicht: {kriterien_text}"
        )


class EmailError(RegistrierungsFehler):
    """Fehler bei der E-Mail-Adresse."""
    def __init__(self, email, grund="ungültiges Format"):
        self.email = email
        self.grund = grund
        super().__init__(f"E-Mail '{email}' ungültig: {grund}")


class AlterError(RegistrierungsFehler):
    """Fehler beim Alter."""
    def __init__(self, geburtsdatum, min_alter=13):
        self.geburtsdatum = geburtsdatum
        self.min_alter = min_alter
        super().__init__(
            f"Mindestalter von {min_alter} Jahren nicht erreicht"
        )


# --- Validierungs-Funktionen ---

BESTEHENDE_BENUTZER = {"admin", "root", "moderator", "anna42"}

def benutzername_validieren(name):
    if len(name) < 3:
        raise BenutzernameZuKurzError(name)
    if not name.isalnum():
        raise BenutzernameError(
            f"Benutzername '{name}' darf nur Buchstaben und Zahlen enthalten"
        )
    if name.lower() in BESTEHENDE_BENUTZER:
        raise BenutzernameVergebenError(name)
    return True


def passwort_validieren(passwort):
    if len(passwort) < 8:
        raise PasswortZuKurzError(len(passwort))

    fehlend = []
    if not re.search(r"[A-Z]", passwort):
        fehlend.append("Großbuchstabe")
    if not re.search(r"[a-z]", passwort):
        fehlend.append("Kleinbuchstabe")
    if not re.search(r"[0-9]", passwort):
        fehlend.append("Zahl")
    if not re.search(r"[!@#$%^&*]", passwort):
        fehlend.append("Sonderzeichen (!@#$%^&*)")

    if fehlend:
        raise PasswortZuSchwachError(fehlend)
    return True


def email_validieren(email):
    if "@" not in email:
        raise EmailError(email, "fehlendes @-Zeichen")
    if not re.match(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", email):
        raise EmailError(email)
    return True


def alter_validieren(geburtsdatum_str):
    try:
        geburtsdatum = date.fromisoformat(geburtsdatum_str)
    except ValueError:
        raise RegistrierungsFehler(
            f"Ungültiges Datumsformat: '{geburtsdatum_str}' "
            "(erwartet: JJJJ-MM-TT)"
        )

    heute = date.today()
    alter = (heute - geburtsdatum).days // 365
    if alter < 13:
        raise AlterError(geburtsdatum, min_alter=13)
    return True


def registrieren(benutzername, passwort, email, geburtsdatum):
    """Registriert einen neuen Benutzer nach Validierung aller Daten."""
    benutzername_validieren(benutzername)
    passwort_validieren(passwort)
    email_validieren(email)
    alter_validieren(geburtsdatum)

    return {
        "benutzername": benutzername,
        "email": email,
        "registriert": True,
    }


# --- Hauptprogramm ---

if __name__ == "__main__":
    test_daten = [
        ("ab", "Test1234!", "test@email.de", "2000-01-15"),
        ("admin", "Test1234!", "test@email.de", "2000-01-15"),
        ("neuuser", "kurz", "test@email.de", "2000-01-15"),
        ("neuuser", "nurklein1!", "test@email.de", "2000-01-15"),
        ("neuuser", "Test1234!", "ungueltig", "2000-01-15"),
        ("neuuser", "Test1234!", "test@email.de", "2020-06-15"),
        ("neuuser", "Test1234!", "test@email.de", "2000-01-15"),
    ]

    for daten in test_daten:
        try:
            ergebnis = registrieren(*daten)
            print(f"Registrierung erfolgreich: {ergebnis}")
        except BenutzernameZuKurzError as e:
            print(f"[Benutzername] {e}")
        except BenutzernameVergebenError as e:
            print(f"[Benutzername] {e}")
        except PasswortZuKurzError as e:
            print(f"[Passwort] {e}")
        except PasswortZuSchwachError as e:
            print(f"[Passwort] {e}")
        except EmailError as e:
            print(f"[E-Mail] {e}")
        except AlterError as e:
            print(f"[Alter] {e}")
        except RegistrierungsFehler as e:
            print(f"[Registrierung] {e}")
        print("---")

Übungen

Übung 1: Bankkonto-Exceptions

Erstelle Exception-Klassen für ein Bankkonto-System:

"""
Aufgabe:
1. Erstelle eine Basis-Exception 'BankError'
2. Erstelle 'KontoNichtGefundenError' (mit Kontonummer)
3. Erstelle 'UnzureichendesMittelError' (mit Betrag und Kontostand)
4. Erstelle 'UngueltigerBetragError' (für negative Beträge)
5. Schreibe eine Klasse 'Bankkonto' mit den Methoden:
   - einzahlen(betrag)
   - abheben(betrag)
   - ueberweisen(ziel_konto, betrag)
"""

class BankError(Exception):
    pass

# Dein Code hier!

Übung 2: Dateiformat-Validierung

Erstelle Exceptions für einen Dateiformat-Validator:

"""
Aufgabe:
1. Erstelle 'DateiFehler' als Basis-Exception
2. Erstelle 'DateiZuGrossError' (mit tatsächlicher und maximaler Größe)
3. Erstelle 'UngueltigesFormatError' (mit Dateiname und erlaubten Formaten)
4. Schreibe eine Funktion 'datei_pruefen(name, groesse_mb)',
   die prüft:
   - Dateigröße max. 10 MB
   - Erlaubte Formate: .jpg, .png, .pdf
"""

# Dein Code hier!

Übung 3: Rezept-Validator

Erstelle ein vollständiges Validierungssystem für Kochrezepte:

"""
Aufgabe:
1. Erstelle Exception-Hierarchie:
   - RezeptError
     - ZutatFehler (fehlende oder ungültige Zutat)
     - MengenFehler (ungültige Mengenangabe)
     - ZeitFehler (ungültige Zubereitungszeit)
2. Jede Exception soll hilfreiche Attribute und Meldungen haben
3. Schreibe eine Funktion 'rezept_validieren(rezept_dict)'
   die ein Rezept-Dictionary prüft
"""

# Dein Code hier!

Pro-Tipp: In der Praxis gruppierst du deine Exception-Klassen oft in einer eigenen Datei namens exceptions.py. So können andere Module sie einfach importieren:

# exceptions.py
class AppError(Exception): ...
class ValidierungsFehler(AppError): ...
class DatenbankError(AppError): ...

# andere_datei.py
from exceptions import ValidierungsFehler, DatenbankError

Wenn du Bibliotheken oder APIs entwickelst, ist eine saubere Exception-Hierarchie besonders wichtig: Nutzer deines Codes sollen gezielt die Fehler abfangen können, die sie behandeln wollen, ohne jede einzelne Exception kennen zu müssen — dank der Hierarchie reicht es, die Basis-Exception zu fangen.

Zurück zum Python Kurs