Debugging-Techniken in Python
Lerne verschiedene Debugging-Methoden kennen, von print()-Debugging bis zum VS Code Debugger, um Fehler in deinem Python-Code effizient zu finden.
Debugging-Techniken in Python
Fehler im Code zu finden ist eine Kunst. Manche Entwickler verbringen mehr Zeit mit Debugging als mit dem eigentlichen Programmieren! In diesem Tutorial lernst du die wichtigsten Techniken, um Fehler in deinem Python-Code schnell und systematisch aufzuspüren.
Was ist Debugging?
Der Begriff “Debugging” kommt angeblich von einer echten Motte (engl. “bug”), die 1947 in einem Computer gefunden wurde und eine Störung verursachte. Seitdem nennen wir das Finden und Beheben von Programmfehlern “Debugging”.
Debugging bedeutet im Wesentlichen:
- Erkennen, dass ein Fehler existiert
- Lokalisieren, wo der Fehler auftritt
- Verstehen, warum der Fehler auftritt
- Beheben des Fehlers
- Überprüfen, dass die Behebung funktioniert
print()-Debugging (Quick & Dirty)
Die einfachste Methode — und oft die erste, die du verwendest:
def durchschnitt_berechnen(zahlen):
print(f"Eingabe: {zahlen}") # Was kommt rein?
summe = sum(zahlen)
print(f"Summe: {summe}") # Stimmt die Summe?
anzahl = len(zahlen)
print(f"Anzahl: {anzahl}") # Stimmt die Anzahl?
ergebnis = summe / anzahl
print(f"Ergebnis: {ergebnis}") # Was kommt raus?
return ergebnis
# Test
durchschnitt_berechnen([10, 20, 30])
# Eingabe: [10, 20, 30]
# Summe: 60
# Anzahl: 3
# Ergebnis: 20.0
Vorteile: Schnell, einfach, braucht keine Werkzeuge.
Nachteile: Muss danach wieder entfernt werden. Bei vielen print-Ausgaben verliert man den Überblick. Verlangsamt den Code.
Tipps für besseres print()-Debugging
# Tipp 1: Verwende Trennlinien
print("=" * 50)
print(f"Variable x = {x}")
print("=" * 50)
# Tipp 2: Markiere deine Debug-Prints
print(f"[DEBUG] Wert von x: {x}")
print(f"[DEBUG] Schleife Iteration {i}")
# Tipp 3: Nutze Bedingungen
if x < 0:
print(f"[WARNUNG] x ist negativ: {x}")
f-String Debugging (f”{variable=}”)
Seit Python 3.8 gibt es eine elegante Kurzform:
name = "Anna"
alter = 25
punkte = [85, 92, 78, 95]
# Das "=" Zeichen nach dem Variablennamen zeigt Name UND Wert
print(f"{name=}") # name='Anna'
print(f"{alter=}") # alter=25
print(f"{punkte=}") # punkte=[85, 92, 78, 95]
# Funktioniert auch mit Ausdrücken!
print(f"{len(punkte)=}") # len(punkte)=4
print(f"{sum(punkte)/len(punkte)=}") # sum(punkte)/len(punkte)=87.5
print(f"{alter > 18=}") # alter > 18=True
# Mit Formatierung
preis = 19.99
print(f"{preis=:.2f}") # preis=19.99
Das spart viel Tipparbeit im Vergleich zu:
# Vorher (umständlich)
print(f"name = {name}, alter = {alter}")
# Nachher (elegant)
print(f"{name=}, {alter=}")
Das Logging-Modul (besser als print!)
Für professionelles Debugging solltest du statt print() das logging-Modul verwenden:
import logging
# Logging einrichten
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(levelname)s - %(message)s"
)
def dividiere(a, b):
logging.debug(f"dividiere({a}, {b}) aufgerufen")
if b == 0:
logging.error("Division durch Null versucht!")
return None
ergebnis = a / b
logging.info(f"Ergebnis: {ergebnis}")
return ergebnis
dividiere(10, 3)
# 2026-02-10 14:30:00 - DEBUG - dividiere(10, 3) aufgerufen
# 2026-02-10 14:30:00 - INFO - Ergebnis: 3.3333333333333335
dividiere(10, 0)
# 2026-02-10 14:30:01 - DEBUG - dividiere(10, 0) aufgerufen
# 2026-02-10 14:30:01 - ERROR - Division durch Null versucht!
Die fünf Log-Level
import logging
logging.debug("Detaillierte Diagnoseinformationen") # Level 10
logging.info("Bestätigung, dass alles funktioniert") # Level 20
logging.warning("Etwas Unerwartetes ist passiert") # Level 30
logging.error("Ein Fehler ist aufgetreten") # Level 40
logging.critical("Schwerwiegender Fehler! Programm kann nicht fortfahren") # Level 50
Du stellst das Log-Level ein, um zu steuern, welche Meldungen angezeigt werden:
# Nur Warnungen und schlimmer anzeigen
logging.basicConfig(level=logging.WARNING)
logging.debug("Wird NICHT angezeigt")
logging.info("Wird NICHT angezeigt")
logging.warning("Wird angezeigt") # Ab hier aufwärts
logging.error("Wird angezeigt")
logging.critical("Wird angezeigt")
Logging in eine Datei
import logging
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
filename="app.log", # In Datei statt Konsole
filemode="a" # Anhängen statt Überschreiben
)
logging.info("Anwendung gestartet")
logging.error("Datenbank nicht erreichbar")
Warum Logging besser als print() ist
# 1. Log-Level: Du steuerst, was angezeigt wird
# In der Entwicklung: level=DEBUG (alles sehen)
# In der Produktion: level=WARNING (nur Probleme sehen)
# 2. Timestamps: Du siehst WANN etwas passiert ist
# 3. Dateiausgabe: Logs gehen nicht verloren
# 4. Kein Aufräumen nötig: Logging-Aufrufe bleiben im Code,
# du änderst nur das Level
# 5. Verschiedene Ziele: Konsole, Datei, E-Mail, Server...
Python Debugger (pdb / breakpoint())
Der eingebaute Python Debugger lässt dich den Code Schritt für Schritt durchgehen:
def fibonacci(n):
if n <= 1:
return n
a, b = 0, 1
breakpoint() # Hier hält das Programm an (seit Python 3.7)
for i in range(2, n + 1):
a, b = b, a + b
return b
fibonacci(10)
Wenn das Programm am breakpoint() anhält, kannst du Befehle eingeben:
# Wichtige pdb-Befehle:
# n (next) - Nächste Zeile ausführen
# s (step) - In Funktion hineinspringen
# c (continue) - Weiterlaufen bis zum nächsten Breakpoint
# p variable - Variable ausgeben
# pp variable - Variable hübsch formatiert ausgeben
# l (list) - Umgebenden Code anzeigen
# w (where) - Aufrufstapel anzeigen
# q (quit) - Debugger beenden
Ein Beispiel einer pdb-Sitzung:
> /pfad/zu/datei.py(6)fibonacci()
-> for i in range(2, n + 1):
(Pdb) p n
10
(Pdb) p a
0
(Pdb) p b
1
(Pdb) n
-> a, b = b, a + b
(Pdb) n
-> for i in range(2, n + 1):
(Pdb) p a, b
(1, 1)
(Pdb) c
Die ältere Methode (vor Python 3.7)
import pdb
def meine_funktion():
x = 10
pdb.set_trace() # Entspricht breakpoint()
y = x + 5
return y
VS Code Debugger einrichten
Der VS Code Debugger ist noch komfortabler als pdb. So richtest du ihn ein:
1. Python-Extension installieren
Installiere die offizielle Python-Extension von Microsoft in VS Code.
2. Launch-Konfiguration erstellen
Erstelle eine Datei .vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Aktuelle Datei",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal"
}
]
}
3. Breakpoints setzen
- Klicke links neben eine Zeilennummer — es erscheint ein roter Punkt
- Oder drücke F9 auf der gewünschten Zeile
4. Debugger starten
- Drücke F5 oder klicke auf “Run and Debug”
- Das Programm startet und hält am ersten Breakpoint an
Breakpoints setzen
In VS Code gibt es verschiedene Arten von Breakpoints:
# Normaler Breakpoint: Klick links neben Zeilennummer
# Bedingter Breakpoint: Rechtsklick -> "Add Conditional Breakpoint"
# z.B. Bedingung: i == 50 (hält nur an wenn i gleich 50 ist)
# Logpoint: Rechtsklick -> "Add Logpoint"
# Gibt eine Nachricht aus, ohne anzuhalten
# z.B.: "Aktuelle Iteration: {i}, Wert: {wert}"
Bedingte Breakpoints sind besonders nützlich in Schleifen:
for i in range(1000):
ergebnis = komplexe_berechnung(i)
# Breakpoint mit Bedingung: ergebnis < 0
# Hält nur an, wenn das Ergebnis negativ wird
speichere(ergebnis)
Step Over, Step Into, Step Out
Wenn der Debugger an einem Breakpoint anhält, hast du diese Steuerungsmöglichkeiten:
Step Over (F10)
Führt die aktuelle Zeile komplett aus und geht zur nächsten weiter. Funktionsaufrufe werden vollständig ausgeführt, ohne hineinzuspringen:
def gruss(name):
return f"Hallo {name}"
name = "Anna" # <- Breakpoint hier
nachricht = gruss(name) # Step Over: gruss() wird ausgeführt, man sieht nur das Ergebnis
print(nachricht) # Nächster Halt
Step Into (F11)
Springt in einen Funktionsaufruf hinein:
name = "Anna"
nachricht = gruss(name) # Step Into: springt IN die gruss()-Funktion
print(nachricht)
Step Out (Shift+F11)
Führt die aktuelle Funktion bis zum Ende aus und springt zum Aufrufer zurück:
def gruss(name):
vorname = name.split()[0] # <- Du bist hier drin
return f"Hallo {vorname}" # Step Out: springt zurück zum Aufrufer
Zusammenfassung der Debugger-Steuerung
F5 - Starten / Fortfahren (Continue)
F10 - Step Over (nächste Zeile, ohne in Funktionen zu springen)
F11 - Step Into (in Funktion hineinspringen)
Shift+F11 - Step Out (aus Funktion herausspringen)
Ctrl+Shift+F5 - Neustart
Shift+F5 - Stoppen
Häufige Fehlerquellen in Python
Hier sind die Klassiker, die dich viel Zeit kosten können:
1. Veränderbare Standardargumente
# FALSCH - die Liste wird zwischen Aufrufen geteilt!
def element_hinzufuegen(element, liste=[]):
liste.append(element)
return liste
print(element_hinzufuegen("a")) # ['a']
print(element_hinzufuegen("b")) # ['a', 'b'] -- Überraschung!
# RICHTIG
def element_hinzufuegen(element, liste=None):
if liste is None:
liste = []
liste.append(element)
return liste
2. Vergleich vs. Zuweisung
# Fehler: = statt ==
if x = 5: # SyntaxError in Python (zum Glück!)
print("x ist 5")
# Richtig:
if x == 5:
print("x ist 5")
# Achtung bei is vs ==
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # True (gleicher Inhalt)
print(a is b) # False (verschiedene Objekte!)
3. Einrückungs-Fehler
# Unsichtbarer Fehler: Mix aus Tabs und Spaces
def funktion():
if True:
print("Tab") # Tab-Einrückung
print("Space") # Space-Einrückung -- kann Fehler verursachen!
# Tipp: In VS Code "Whitespace anzeigen" aktivieren
# Einstellungen -> "Render Whitespace" -> "all"
4. Off-by-One-Fehler
# range(5) gibt 0, 1, 2, 3, 4 -- NICHT 5!
for i in range(5):
print(i) # 0 bis 4
# Liste mit 5 Elementen: Index 0 bis 4
liste = [10, 20, 30, 40, 50]
# liste[5] # IndexError! Letzter Index ist 4
5. Ganzzahl-Division
# In Python 3 gibt / immer ein Float zurück
print(7 / 2) # 3.5
print(7 // 2) # 3 (Ganzzahl-Division)
# Achtung bei Typen!
print(type(7 / 1)) # <class 'float'> -- auch wenn das Ergebnis "glatt" ist!
Traceback lesen und verstehen
Wenn Python einen Fehler meldet, zeigt es einen Traceback. Den zu lesen ist eine der wichtigsten Debugging-Fähigkeiten:
def funktion_a():
return funktion_b()
def funktion_b():
return funktion_c()
def funktion_c():
return 1 / 0 # Fehler!
funktion_a()
Python zeigt:
Traceback (most recent call last): # Von oben nach unten lesen
File "test.py", line 10, in <module> # Aufruf in der Hauptdatei
funktion_a()
File "test.py", line 2, in funktion_a # funktion_a ruft funktion_b auf
return funktion_b()
File "test.py", line 5, in funktion_b # funktion_b ruft funktion_c auf
return funktion_c()
File "test.py", line 8, in funktion_c # HIER ist der Fehler!
return 1 / 0
ZeroDivisionError: division by zero # Fehlertyp und Beschreibung
So liest du einen Traceback:
- Beginne unten — dort steht der Fehlertyp und die Beschreibung
- Die letzte Datei/Zeile vor dem Fehler ist die Stelle, an der der Fehler auftritt
- Gehe nach oben, um die Aufrufkette zu verstehen
Traceback bei verschachtelten Fehlern
try:
int("abc")
except ValueError as e:
raise RuntimeError("Konvertierung fehlgeschlagen") from e
Traceback (most recent call last):
File "test.py", line 2, in <module>
int("abc")
ValueError: invalid literal for int() with base 10: 'abc'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "test.py", line 4, in <module>
raise RuntimeError("Konvertierung fehlgeschlagen") from e
RuntimeError: Konvertierung fehlgeschlagen
Rubber Duck Debugging
Eine der überraschend effektivsten Debugging-Methoden braucht keinen Computer — nur eine Gummiente (oder ein beliebiges Objekt):
- Stelle eine Gummiente auf deinen Schreibtisch
- Erkläre der Ente Zeile für Zeile, was dein Code tun soll
- Erkläre, was der Code tatsächlich tut
- Bemerke den Unterschied
Warum funktioniert das? Indem du den Code laut erklärst, zwingst du dein Gehirn, jeden Schritt bewusst durchzudenken. Dabei fallen dir Fehler auf, die du beim stillen Lesen übersehen hast.
# "Also, Ente... Zuerst erstelle ich eine leere Liste..."
ergebnisse = []
# "Dann gehe ich durch jeden Benutzer..."
for benutzer in benutzer_liste:
# "Und prüfe, ob das Alter über 18 ist..."
if benutzer["alter"] > 18:
# "Moment... es soll >= 18 sein, nicht > 18!"
# BUG GEFUNDEN!
ergebnisse.append(benutzer)
Praxis-Beispiele
Beispiel 1: Mysteriöser Fehler finden
def notenstatistik(noten):
"""Berechnet Statistiken für eine Notenliste."""
import logging
logging.basicConfig(level=logging.DEBUG)
logging.debug(f"Eingabe: {noten}")
logging.debug(f"Typ: {type(noten)}")
logging.debug(f"Länge: {len(noten)}")
if not noten:
logging.warning("Leere Notenliste!")
return None
gesamt = sum(noten)
anzahl = len(noten)
durchschnitt = gesamt / anzahl
logging.debug(f"Summe: {gesamt}, Anzahl: {anzahl}")
logging.debug(f"Durchschnitt: {durchschnitt}")
beste = min(noten) # In Deutschland: 1 = beste Note
schlechteste = max(noten)
logging.info(f"Statistik berechnet: Schnitt={durchschnitt:.2f}")
return {
"durchschnitt": round(durchschnitt, 2),
"beste": beste,
"schlechteste": schlechteste,
"anzahl": anzahl,
}
# Test
ergebnis = notenstatistik([2, 1, 3, 2, 1, 4, 2])
print(ergebnis)
Beispiel 2: Systematisches Debugging mit Divide and Conquer
Wenn du einen Fehler in einer langen Funktion suchst, teile sie in Hälften:
def komplexe_verarbeitung(daten):
# Schritt 1-5
zwischenergebnis_1 = schritt_1_bis_5(daten)
print(f"[CHECK 1] {zwischenergebnis_1=}") # Ist das korrekt?
# Schritt 6-10
zwischenergebnis_2 = schritt_6_bis_10(zwischenergebnis_1)
print(f"[CHECK 2] {zwischenergebnis_2=}") # Ist das korrekt?
# Schritt 11-15
endergebnis = schritt_11_bis_15(zwischenergebnis_2)
print(f"[CHECK 3] {endergebnis=}") # Ist das korrekt?
return endergebnis
# Wenn CHECK 1 korrekt ist, aber CHECK 2 nicht,
# liegt der Fehler in schritt_6_bis_10()!
# Dann teilst du diese Funktion weiter auf.
Beispiel 3: assert für Annahmen prüfen
def rabatt_berechnen(preis, prozent):
# Eigene Annahmen mit assert prüfen
assert isinstance(preis, (int, float)), f"Preis muss Zahl sein, ist {type(preis)}"
assert preis >= 0, f"Preis darf nicht negativ sein: {preis}"
assert 0 <= prozent <= 100, f"Prozent muss zwischen 0 und 100 liegen: {prozent}"
rabatt = preis * (prozent / 100)
endpreis = preis - rabatt
assert endpreis >= 0, f"Endpreis ist negativ: {endpreis}"
return round(endpreis, 2)
# Tests
print(rabatt_berechnen(100, 20)) # 80.0
# print(rabatt_berechnen(-50, 20)) # AssertionError: Preis darf nicht negativ sein
# print(rabatt_berechnen(100, 150)) # AssertionError: Prozent muss zwischen 0 und 100 liegen
Übungen
Übung 1: Fehler finden
In jedem Codeblock ist ein Fehler versteckt. Finde und behebe ihn:
# Bug 1: Warum gibt die Funktion immer None zurück?
def maximum(zahlen):
max_wert = 0
for zahl in zahlen:
if zahl > max_wert:
max_wert = zahl
# Hinweis: Was fehlt hier?
# Bug 2: Warum funktioniert die Suche nicht?
def suche_benutzer(benutzer_liste, gesuchter_name):
for benutzer in benutzer_liste:
if benutzer["name"] == gesuchter_name:
return True
else:
return False
# Bug 3: Warum gibt die Funktion falsche Ergebnisse?
def fibonacci(n):
a, b = 0, 1
ergebnisse = []
for i in range(n):
ergebnisse.append(a)
a = b
b = a + b # Hinweis: Genau hinsehen!
return ergebnisse
Übung 2: Debugging-Strategie
Nutze systematisches Debugging (print, logging oder pdb), um den Fehler in diesem Code zu finden:
def woerter_zaehlen(text):
"""Zählt die Häufigkeit jedes Wortes im Text."""
zaehler = {}
woerter = text.split(" ")
for wort in woerter:
wort = wort.lower()
if wort in zaehler:
zaehler[wort] = +1 # Hier steckt der Fehler!
else:
zaehler[wort] = 1
return zaehler
# Test:
text = "die Katze und die Maus und die Katze"
print(woerter_zaehlen(text))
# Erwartet: {'die': 3, 'katze': 2, 'und': 2, 'maus': 1}
# Tatsächlich: {'die': 1, 'katze': 1, 'und': 1, 'maus': 1}
Übung 3: VS Code Debugger üben
Erstelle die folgende Datei und übe mit dem VS Code Debugger:
"""
Aufgabe:
1. Setze einen Breakpoint auf Zeile "ergebnis = ..."
2. Starte den Debugger (F5)
3. Beobachte die Variablen im Debug-Panel
4. Nutze Step Over (F10) um durch die Schleife zu gehen
5. Finde heraus, in welcher Iteration der Fehler auftritt
"""
def geheime_berechnung(werte):
ergebnisse = []
for i, wert in enumerate(werte):
if wert != 0:
ergebnis = 100 / wert
else:
ergebnis = -1 # Sonderfall
ergebnisse.append(ergebnis)
return ergebnisse
daten = [25, 50, 0, 10, 5, 0, 20]
print(geheime_berechnung(daten))
Pro-Tipp: Der f-String Debug-Trick f"{variable=}" funktioniert auch mit komplexen Ausdrücken. Nutze ihn, um schnell den Zustand deines Programms zu überprüfen, ohne lange print-Anweisungen zu schreiben:
daten = {"name": "Anna", "punkte": [85, 92, 78]}
print(f"{daten['punkte'][0]=}") # daten['punkte'][0]=85
print(f"{sum(daten['punkte'])=}") # sum(daten['punkte'])=255
print(f"{len(daten)=}") # len(daten)=2
print(f"{'name' in daten=}") # 'name' in daten=True
Kombiniere das mit logging.debug() für Debugging-Ausgaben, die du mit einem einzigen Level-Wechsel abschalten kannst, anstatt alle print-Anweisungen manuell entfernen zu müssen.