Zum Inhalt springen
Python Anfänger 2 min

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.

Aktualisiert:

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:

  1. Erkennen, dass ein Fehler existiert
  2. Lokalisieren, wo der Fehler auftritt
  3. Verstehen, warum der Fehler auftritt
  4. Beheben des Fehlers
  5. Überprüfen, dass die Behebung funktioniert

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:

  1. Beginne unten — dort steht der Fehlertyp und die Beschreibung
  2. Die letzte Datei/Zeile vor dem Fehler ist die Stelle, an der der Fehler auftritt
  3. 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):

  1. Stelle eine Gummiente auf deinen Schreibtisch
  2. Erkläre der Ente Zeile für Zeile, was dein Code tun soll
  3. Erkläre, was der Code tatsächlich tut
  4. 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.

Zurück zum Python Kurs