Eigene Exceptions in Python erstellen
Lerne, wie du eigene Exception-Klassen erstellst, um aussagekräftige Fehlermeldungen in deinen Python-Programmen zu erzeugen.
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.