Im Gegensatz zu den meisten Kursen im Studium sind Fehler beim Programmieren normal und nicht sofort schlecht, denn anders als in den meisten Kursen erhältst du beim Programmieren sofortige Rückmeldung über diese Fehler und wie du sie beheben kannst. Dieses Tutorial zeigt dir,
wie du Fehlermeldungen liest, die eigentliche Ursache eines Problems findest und gezielt behebst.
Die Aufgaben beginnen einfach und werden schrittweise anspruchsvoller. Bearbeite sie der Reihe nach, denn
jede Übung baut auf der vorherigen auf.
Tipp: Lies immer zuerst die letzte Zeile des Tracebacks.
Block 1 · Aufgaben 1–3
Den Fehlertyp erkennen
Jede Python-Fehlermeldung beginnt mit einem Typ, zum Beispiel TypeError oder ValueError.
Dieser Typ sagt dir sofort, welche Art von Problem vorliegt, noch bevor du eine einzige Zeile Code anschaust.
Übe hier, Fehlermeldungen schnell einzuordnen.
Ziel: Fehlertypen kennenlernen
TypeError
Eine Operation wird auf inkompatible Typen angewendet z.B. int([1, 2, 3]).
ValueError
Der Typ stimmt, aber der Inhalt passt nicht z.B. a, b = 1, 2, 3.
NameError
Ein Name (Variable, Funktion) wird verwendet, der nie definiert wurde.
IndexError
Ein Listenindex liegt ausserhalb des gültigen Bereichs z.B. liste[5] bei einer 3-Element-Liste.
KeyError
Ein Dictionary-Schlüssel existiert nicht z.B. d["email"] wenn "email" nicht im Dict ist.
AttributeError
Ein Objekt hat kein Attribut oder keine Methode dieses Namens z.B. hund.alter wenn alter nie definiert wurde.
ZeroDivisionError
Division durch null z.B. 10 / 0 oder sum / len(liste) wenn die Liste leer ist.
FileNotFoundError
Eine Datei existiert nicht am angegebenen Pfad oder das Arbeitsverzeichnis stimmt nicht.
Aufgabe 1: Welcher Fehlertyp liegt vor?
name = input("Dein Name: ")
print("Hallo, " + 42)
TypeError tritt auf, wenn eine Operation auf inkompatible Typen angewendet wird.
Python kann einen String ("Hallo, ") und eine Ganzzahl (42) nicht direkt verketten —
du müsstest erst str(42) schreiben. Der Wert 42 ist an sich nicht falsch, nur der Typ passt nicht.
Aufgabe 2: Welcher Fehlertyp liegt vor?
alter = int("zwanzig")
ValueError bedeutet: der Typ stimmt (es ist ein String, und int() akzeptiert Strings),
aber der Inhalt passt nicht. "zwanzig" ist kein gültiges Zahlformat.
Im Unterschied dazu würde int("20") funktionieren, denn dort ist der Wert konvertierbar.
NameError heisst: Python kennt diesen Namen schlicht nicht. vorname wurde weder
als Variable noch als Parameter definiert. Häufige Ursachen sind Tippfehler, vergessene Zuweisung oder ein
falscher Gültigkeitsbereich (Scope).
Block 2 · Aufgaben 4–6
Einen Traceback lesen
Ein Traceback zeigt den Aufrufstapel und zwar von der äussersten Funktion bis zur Zeile, die den Fehler ausgelöst hat.
Lies ihn von unten nach oben: Die letzte eigene Codezeile vor der Fehlermeldung ist meistens der Ausgangspunkt.
Klicke auf die Zeile, an der du zuerst mit der Analyse beginnen würdest.
Ziel: die relevante Zeile im Traceback finden
Aufgabe 4: Welche Zeile ist der Ausgangspunkt des Fehlers?
Traceback (most recent call last)
Die Zeile return sum(zahlen) / len(zahlen) ist der direkte Auslöser: len(zahlen) ergibt 0,
wenn die Liste leer ist. Der eigentliche Denkfehler liegt aber eine Ebene höher: Die Funktion sollte prüfen,
ob die Liste überhaupt Elemente enthält, bevor sie dividiert.
Aufgabe 5: Welche Zeile ist der Ausgangspunkt des Fehlers?
Traceback (most recent call last)
KeyError bedeutet: der Schlüssel existiert nicht im Dictionary. Das Dictionary benutzer
hat die Schlüssel "name" und "alter", aber keinen Schlüssel "email".
Um das sicher abzufragen, kannst du b.get("email") verwenden. Das gibt None zurück statt einen Fehler zu werfen.
Aufgabe 6: Welche Zeile ist der Ausgangspunkt des Fehlers?
Traceback (most recent call last)
IndexError heisst: der Index liegt ausserhalb des gültigen Bereichs. Die Liste hat drei Elemente
(Indizes 0, 1, 2), aber liste[5] greift auf den sechsten Platz zu und der existiert nicht.
Merke: In Python ist der letzte gültige Index immer len(liste) - 1.
Block 3 · Aufgaben 7–8
Die eigentliche Ursache benennen
Den Fehlertyp kennen ist der erste Schritt. Der zweite ist zu verstehen, warum er aufgetreten ist.
Analysiere den Code und wähle die Erklärung, die die Ursache und nicht nur das Symptom beschreibt.
Ziel: von Symptom zur Ursache
Aufgabe 7: Was ist die eigentliche Ursache des Fehlers?
import os
def konfiguration_laden(pfad):
with open(pfad) as f:
return f.read()
konfiguration_laden("einstellungen.txt")
Python wirft FileNotFoundError: [Errno 2] No such file or directory: 'einstellungen.txt'
FileNotFoundError bedeutet einfach: Python kann die Datei am angegebenen Ort nicht finden.
Mögliche Ursachen: Die Datei existiert gar nicht, das Skript läuft in einem anderen Verzeichnis als erwartet,
oder der Pfad enthält einen Tippfehler. Mit os.getcwd() kannst du das aktuelle Arbeitsverzeichnis prüfen.
Aufgabe 8: Was ist die eigentliche Ursache des Fehlers?
class Tier:
def __init__(self, name):
self.name = name
hund = Tier("Bello")
print(hund.alter)
Python wirft AttributeError: 'Tier' object has no attribute 'alter'
AttributeError tritt auf, wenn du auf ein Attribut oder eine Methode zugreifst, die ein Objekt nicht besitzt.
Hier definiert __init__ nur self.name, aber kein self.alter.
Der Fix: entweder self.alter = None in __init__ hinzufügen, oder alter als Parameter übergeben.
Block 4 · Aufgaben 9–11
Den richtigen Fix auswählen
Für jeden Fehler gibt es mehrere Wege, aber nicht alle beheben die eigentliche Ursache.
Einige Fixes verstecken das Problem nur oder erzeugen neue Fehler an anderer Stelle.
Wähle jeweils den Fix, der die Ursache direkt und sauber löst.
Ziel: die Ursache beheben, nicht das Symptom
Aufgabe 9: Welcher Fix ist am sinnvollsten?
werte = ["3", "4", "5"]
gesamt = 0
for w in werte:
gesamt += w # TypeError hier
print(gesamt)
Die Liste enthält Strings, obwohl Zahlen gemeint sind. Der sauberste Fix ist, beim Einlesen oder bei der Verarbeitung
zu konvertieren: gesamt += int(w). Noch besser: Die Liste gleich als Zahlen anlegen ([3, 4, 5]).
Den Fehler zu ignorieren oder gesamt in einen String umzuwandeln löst das eigentliche Datenproblem nicht.
Aufgabe 10: Welcher Fix ist am sinnvollsten?
def ersten_eintrag(liste):
return liste[0] # IndexError wenn Liste leer
ergebnis = ersten_eintrag([])
Der sauberste Fix ist, vor dem Zugriff zu prüfen: return liste[0] if liste else None.
So kommuniziert die Funktion klar, dass sie nichts zurückgeben kann. Ein except-Block, der 0 zurückgibt,
ist irreführend: Was, wenn 0 ein gültiger Wert in der Liste ist? Und Dummy-Elemente einzufügen verschiebt das
Problem nur auf den Aufrufer.
Aufgabe 11: Welcher Fix ist am sinnvollsten?
eingabe = input("Gib eine Zahl ein: ")
ergebnis = 100 / int(eingabe) # möglicher ZeroDivisionError oder ValueError
print(ergebnis)
Benutzereingaben sind grundsätzlich unzuverlässig. Der richtige Umgang ist, konkrete Fehler einzeln abzufangen
und dem Benutzer zu erklären, was schiefgelaufen ist:
try: ergebnis = 100 / int(eingabe) except ValueError: print("Bitte eine ganze Zahl eingeben.") except ZeroDivisionError: print("Division durch null nicht möglich.")
Ein nacktes except: verschluckt auch Tastaturabbrüche (KeyboardInterrupt) — das sollte man vermeiden.
Block 5 · Aufgaben 12–13
Debugging als Methode
Gutes Debugging ist kein Raten — es ist ein systematischer Prozess. Hier geht es darum,
die richtigen Fragen in der richtigen Reihenfolge zu stellen.
Ziel: eine Debugging-Gewohnheit entwickeln
1
Fehlermeldung lesen: Welcher Typ? Was sagt die letzte Zeile des Tracebacks?
2
Erwartung formulieren: Was hat der Code an dieser Stelle erwartet, was hat er bekommen?
3
Kleinste Änderung: Eine Sache ändern, die die Ursache behebt und nicht mehrere auf einmal.
4
Verhalten prüfen: Erneut ausführen und das Verhalten prüfen, nicht nur das Fehlen der Fehlermeldung.
5
Verstehen dokumentieren: Was war die Ursache? Ein kurzer Kommentar verhindert denselben Fehler morgen.
Aufgabe 12: Du hast einen Fehler gefunden. Was ist der nächste richtige Schritt?
def verbinde(a, b, c):
return a + b + c
# Aufruf:
verbinde("Hallo", " ", 42) # TypeError
Immer nur eine Änderung auf einmal, denn so weisst du, was den Fehler behoben hat. Hier ist die Ursache klar:
42 ist eine Zahl, aber die Funktion verkettet Strings. Die kleinstmögliche Korrektur ist,
42 durch "42" zu ersetzen. Mehrere Änderungen gleichzeitig machen es unmöglich zu
wissen, welche davon die Lösung war.
Aufgabe 13: Welche Exception-Behandlung ist in diesem Fall am besten?
Fange immer so spezifisch wie möglich ab. Hier kann nur int() scheitern, und der einzig mögliche
Fehler ist ein ValueError. Ein breites except Exception würde auch unerwartete Fehler
schlucken und das Debuggen erschweren. Und Benutzereingaben nie blind zu vertrauen ist eine Grundregel der
robusten Programmierung.
🎉
Tutorial abgeschlossen!
Du hast alle 13 Pflichtaufgaben erfolgreich bearbeitet. Du kannst jetzt Tracebacks lesen, Fehlertypen einordnen,
Ursachen benennen und gezielt beheben. Das ist das Handwerkszeug für professionelles Debugging in Python.
Wenn du noch einen Schritt weitergehen möchtest: Im optionalen Kapitel unten lernst du, wie man eigene
Fehlerklassen schreibt. Das ist nützlich, sobald die eingebauten Exceptions nicht mehr ausdrucksstark genug sind.
Wenn die eingebauten Exceptions zu allgemein sind, kannst du eigene definieren.
Das macht deinen Code ausdrucksstärker und erleichtert es dem Aufrufer, gezielt auf bestimmte Probleme zu reagieren.
Freiwillig
Stell dir vor, du schreibst ein System, das Übungsabgaben prüft. Ein ValueError wäre zu unspezifisch, da
er aus vielen anderen Teilen des Codes kommen könnte. Eine eigene Exception macht klar, woher das Problem stammt:
class UngueltigeAbgabeError(Exception):
"""Wird ausgelöst, wenn eine Abgabe formal korrekt, aber inhaltlich ungültig ist."""
pass
class LeereAbgabeError(UngueltigeAbgabeError):
"""Spezialisierung: die Abgabe ist leer."""
pass
def abgabe_prüfen(antwort: str) -> None:
if not antwort.strip():
raise LeereAbgabeError("Leere Antworten werden nicht akzeptiert.")
if len(antwort) < 10:
raise UngueltigeAbgabeError("Die Antwort ist zu kurz.")
Durch die Vererbung (LeereAbgabeError erbt von UngueltigeAbgabeError) kann der Aufrufer
entweder gezielt auf den spezifischen Typ oder allgemein auf den Basistyp reagieren.
Aufgabe 14: Von welcher Klasse sollte eine eigene Exception erben?
Eigene Exceptions sollten von Exception erben, nicht von BaseException.
BaseException ist die Wurzel aller Ausnahmen — inklusive KeyboardInterrupt und
SystemExit. Wenn du davon erbst, fängst du mit einem breiten except MeinFehler
versehentlich auch Tastaturabbrüche ab. Exception ist genau für anwendungsdefinierte Fehler gedacht.
Aufgabe 15: Was passiert, wenn du except UngueltigeAbgabeError schreibst, aber ein LeereAbgabeError ausgelöst wird?
try:
abgabe_prüfen("")
except UngueltigeAbgabeError as e:
print("Abgabefehler:", e)
except fängt den genannten Typ und alle Unterklassen davon ab. Da LeereAbgabeError
von UngueltigeAbgabeError erbt, ist ein LeereAbgabeError gleichzeitig auch ein
UngueltigeAbgabeError. Das ist der Hauptvorteil von Vererbungshierarchien bei Exceptions:
Aufrufer können entweder fein- oder grobgranular abfangen.
Aufgabe 16: Welche dieser Exception-Definitionen ist am besten geschrieben?
Die erste Definition folgt allen drei Faustregeln: Sie erbt von Exception, hat einen sprechenden Namen
der das Problem beschreibt (nicht den Ort im Code), und enthält einen Docstring, der erklärt, wann die
Exception ausgelöst wird. Fehler ist zu generisch und erbt von der falschen Basisklasse.
MeinProgrammZeile42Fehler beschreibt den Ort im Code statt das Problem — das ist wenig hilfreich,
wenn die Zeile sich ändert.
Drei Faustregeln für eigene Exceptions:
1. Erbe von Exception, nicht von BaseException, sonst fängst du versehentlich Systemereignisse ab.
2. Gib der Klasse einen sprechenden Namen, der das Problem beschreibt und nicht den Ort im Code.
3. Nutze Vererbung, wenn du zwischen ähnlichen Fehlern unterscheiden möchtest, aber nicht tiefer als zwei Ebenen.