Zum Inhalt springen

cat posts/debuggen-mit-pdb.md

Debuggen mit breakpoint() und pdb

Statt überall print zu streuen: Mit breakpoint() und pdb hältst Du Python-Code gezielt an, inspizierst Variablen und gehst Schritt für Schritt durch den Ablauf.

Wenn etwas nicht funktioniert, ist der erste Reflex oft:

print("bin hier")
print(spieler)
print(raum)
print(argument)

Für eine schnelle Frage ist das völlig okay. Nach einer Weile steckt der Code aber voller Debug-Ausgaben, die wieder entfernt werden müssen. Noch dazu siehst Du immer nur genau die Werte, an deren Ausgabe Du vorher gedacht hast.

In Teil 8 der Python-Serie haben wir Exceptions behandelt. In Logging statt print ging es darum, was ein laufendes Programm dauerhaft protokollieren sollte.

Jetzt geht es um die Situation, in der das Programm vor Dir läuft und Du wissen willst, was genau an dieser Stelle passiert.

Python bringt dafür mit pdb bereits einen Debugger mit.

Du kannst ein Programm anhalten, Variablen ansehen, Ausdrücke ausprobieren, den Call Stack untersuchen und anschließend Zeile für Zeile weiterlaufen.

Debugger statt print-Konfetti

Angenommen, im Dungeon funktioniert das Aufnehmen eines Gegenstands nicht.

Du könntest zunächst ein paar Ausgaben einbauen:

print("verb:", verb)
print("argument:", argument)
print("gegenstaende:", raum.gegenstaende)

Damit bekommst Du einen Snapshot der Werte. Wenn die Ursache dort nicht zu erkennen ist, fügst Du weitere Ausgaben ein, startest das Programm neu und probierst es noch einmal.

Mit einem Debugger hältst Du stattdessen direkt vor der verdächtigen Stelle an.

Dann kannst Du im laufenden Programm fragen:

Was steht gerade in argument?
Wie sieht raum.gegenstaende aus?
Welches Objekt steckt in spieler?
Welche Funktion hat diese Funktion aufgerufen?
Was passiert, wenn ich jetzt genau eine Zeile weitergehe?

Du entscheidest also erst nach dem Anhalten, welche Informationen Du brauchst.

breakpoint(): der einfachste Einstieg

Seit Python 3.7 gibt es die eingebaute Funktion breakpoint():

def verarbeite_befehl(verb, argument, spieler, raum):
    if verb == "nimm":
        breakpoint()

        gegenstand = raum.nimm_gegenstand(argument)
        spieler.nimm(gegenstand)

        print(f"Du nimmst {gegenstand}.")

Sobald Python diese Stelle erreicht, öffnet sich standardmäßig pdb.

Im Terminal erscheint:

(Pdb)

Das Programm ist jetzt angehalten und wartet auf einen Debugger-Befehl.

Du kannst den aktuellen State untersuchen und danach entscheiden, ob Du eine Zeile weitergehen, in einen Funktionsaufruf hineinsteigen oder das Programm normal fortsetzen möchtest.

Ein kleines Beispiel

Lege eine Datei debug_beispiel.py an:

def berechne_schaden(staerke, ruestung):
    schaden = staerke - ruestung

    breakpoint()

    return max(schaden, 0)


def main():
    hp = 100
    schaden = berechne_schaden(25, 10)
    hp = hp - schaden

    print(f"HP: {hp}")


main()

Starte sie:

python debug_beispiel.py

In aktuellen Python-Versionen hält pdb direkt am Breakpoint im aufrufenden Frame an. Die genaue Darstellung enthält Dateiname, Zeilennummer und die aktuelle Quellcodezeile:

> debug_beispiel.py(4)berechne_schaden()
-> breakpoint()
(Pdb)

Die genauen Zeilennummern hängen natürlich von Deiner Datei ab.

Jetzt kannst Du die lokalen Variablen abfragen:

(Pdb) p staerke
25
(Pdb) p ruestung
10
(Pdb) p schaden
15

Mit:

(Pdb) c

läuft das Programm weiter.

Ausgabe:

HP: 85

Schon damit hast Du den grundlegenden pdb-Workflow:

Breakpoint erreichen
        ↓
State untersuchen
        ↓
weiterlaufen oder einzeln weitergehen

Die wichtigsten pdb-Befehle

Die meisten Befehle besitzen eine Kurz- und eine Langform.

Befehl Langform Bedeutung
h help Hilfe anzeigen
l list Quellcode um die aktuelle Stelle anzeigen
ll longlist Quellcode der aktuellen Funktion anzeigen
n next weiter, ohne in aufgerufene Funktionen einzusteigen
s step weiter und gegebenenfalls in einen Funktionsaufruf steigen
r return bis zum Return der aktuellen Funktion laufen
c continue bis zum nächsten Breakpoint weiterlaufen
p Python-Ausdruck auswerten und anzeigen
pp Ergebnis mit pprint anzeigen
a args Argumente der aktuellen Funktion anzeigen
w where Call Stack anzeigen
u up im Stack zum Aufrufer wechseln
d down im Stack wieder zum jüngeren Frame wechseln
b break Breakpoint setzen
q quit Debugging abbrechen

Die Kurzformen sind angenehm, wenn Du pdb häufig benutzt. Am Anfang spricht nichts gegen:

(Pdb) next
(Pdb) step
(Pdb) continue

statt:

(Pdb) n
(Pdb) s
(Pdb) c

Werte mit p und pp ansehen

Der wahrscheinlich häufigste pdb-Befehl ist:

p

Zum Beispiel:

(Pdb) p schaden
15

p ist ein pdb-Kommando zum Auswerten eines Python-Ausdrucks.

Du bist dabei nicht auf einen einzelnen Variablennamen beschränkt:

(Pdb) p staerke - ruestung
15
(Pdb) p schaden > 0
True
(Pdb) p type(schaden)
<class 'int'>

Auch Methodenaufrufe und Comprehensions sind möglich:

(Pdb) p len(spieler.inventar)
2
(Pdb) p [gegenstand.name for gegenstand in spieler.inventar]
['Fackel', 'Schlüssel']

Für längere Datenstrukturen ist pp oft angenehmer:

(Pdb) pp spieler.inventar
[
    Gegenstand(name='Fackel', beschreibung='Eine rußige Fackel.'),
    Gegenstand(name='Schlüssel', beschreibung='Ein schwerer Schlüssel.'),
]

pp verwendet dafür pprint.

Python-Code im Debugger ausführen

pdb kann nicht nur Werte lesen.

Du kannst auch Python-Code ausführen:

(Pdb) spieler.hp = 100
(Pdb) p spieler.hp
100

Damit veränderst Du den echten State des angehaltenen Programms.

Das kann sehr praktisch sein.

Vielleicht möchtest Du ausprobieren, ob der Fehler verschwindet, wenn ein bestimmter Wert anders wäre:

(Pdb) spieler.hp = 1
(Pdb) c

Oder eine Methode testweise aufrufen:

(Pdb) p raum.hat_ausgang("norden")
True

Dabei solltest Du im Kopf behalten, was Du getan hast. Sobald Du Objekte im Debugger veränderst, untersuchst Du nicht mehr exakt denselben Zustand, in dem das Programm ursprünglich angehalten wurde.

Seit Python 3.13 wirken Zuweisungen über pdb auch in optimierten lokalen Scopes unmittelbar auf den aktiven Frame. Das Verhalten solcher Debugger-Zuweisungen ist dadurch wesentlich konsistenter geworden.

Wenn ein Name wie ein pdb-Befehl heißt

Debugger-Befehle und Python-Code teilen sich denselben Prompt.

Hast Du beispielsweise eine Variable:

c = 42

und gibst ein:

(Pdb) c

interpretiert pdb das als continue.

Mit einem führenden ! kannst Du ausdrücklich Python-Code ausführen:

(Pdb) !c
42

Auch eine Zuweisung lässt sich damit eindeutig formulieren:

(Pdb) !spieler.hp = 100

Seit Python 3.13 erkennt pdb Python-Ausdrücke, deren Anfang nur zufällig wie ein Debugger-Befehl aussieht, besser. Bei einer echten Kollision wie dem einzelnen Namen c brauchst Du die Unterscheidung aber weiterhin.

next oder step?

Bei der schrittweisen Fehlersuche brauchst Du vor allem zwei Befehle:

n
s

n: Step Over

n steht für next.

Angenommen, die aktuelle Zeile lautet:

gegenstand = raum.nimm_gegenstand(argument)

Mit:

(Pdb) n

führt Python den gesamten Methodenaufruf aus und hält anschließend wieder in der aktuellen Funktion an.

Du siehst also das Ergebnis, ohne den Ablauf von nimm_gegenstand() einzeln zu verfolgen.

s: Step Into

Mit:

(Pdb) s

steigt pdb dagegen in einen aufgerufenen Python-Frame hinein.

Bei:

gegenstand = raum.nimm_gegenstand(argument)

kannst Du so direkt in nimm_gegenstand() landen und dort weitergehen.

Eine gute Faustregel:

  • Interessiert Dich nur das Ergebnis eines Aufrufs, nimm n.
  • Vermutest Du den Fehler innerhalb des Aufrufs, nimm s.

In grafischen Debuggern heißen dieselben Konzepte meistens Step Over und Step Into.

Mit where, up und down den Call Stack untersuchen

Wenn eine Funktion eine andere aufruft, entsteht ein Call Stack.

Im Dungeon könnte er ungefähr so aussehen:

main()
  verarbeite_befehl()
    raum.nimm_gegenstand()

Mit:

(Pdb) w

zeigt pdb die beteiligten Frames:

  dungeon.py(230)<module>()
-> main()
  dungeon.py(196)main()
-> verarbeite_befehl(...)
  dungeon.py(151)verarbeite_befehl()
-> gegenstand = raum.nimm_gegenstand(argument)
> dungeon.py(92)nimm_gegenstand()
-> gesuchter_name = name.strip().lower()

Der mit > markierte Eintrag ist der aktuell ausgewählte Frame.

Mit:

(Pdb) u

gehst Du eine Ebene nach oben zum Aufrufer.

Mit:

(Pdb) d

wieder nach unten.

Das ist mehr als eine andere Ansicht auf denselben Code. Der gewählte Frame bestimmt auch, welche lokalen Variablen Du gerade untersuchen kannst.

Innerhalb von:

nimm_gegenstand(self, name)

gibt es beispielsweise:

self
name
gesuchter_name

Im aufrufenden verarbeite_befehl() dagegen vielleicht:

verb
argument
spieler
raum

Mit up kannst Du also buchstäblich zurückverfolgen, welche Werte eine Funktion von ihrem Aufrufer erhalten hat.

Funktionsargumente mit args

Dafür gibt es noch einen bequemeren Befehl:

(Pdb) a

beziehungsweise:

(Pdb) args

Beispiel:

(Pdb) a
self = <__main__.Raum object at 0x...>
name = 'fackel'

So siehst Du schnell, mit welchen Argumenten der aktuelle Funktionsaufruf gestartet wurde.

Quellcode mit list und longlist

Wenn Du nicht mehr genau weißt, wo Du gerade bist:

(Pdb) l

zeigt Quellcode um die aktuelle Position:

 88     def nimm_gegenstand(self, name):
 89         gesuchter_name = name.strip().lower()
 90
 91         for gegenstand in self.gegenstaende:
 92  ->         if gegenstand.name.lower() == gesuchter_name:
 93                 self.gegenstaende.remove(gegenstand)

Mit:

(Pdb) ll

bekommst Du den Quellcode der aktuellen Funktion beziehungsweise des aktuellen Frames.

Das ist besonders auf einem Server praktisch, wenn der Editor nicht direkt neben dem Terminal offen ist.

Bis zum Return weiterlaufen

Bist Du mit s in einer Funktion gelandet und möchtest nicht jede weitere Zeile durchgehen:

(Pdb) r

return lässt die aktuelle Funktion bis zu ihrer Rückkehr laufen.

Danach kannst Du den zuletzt beobachteten Rückgabewert mit:

(Pdb) retval

erneut anzeigen.

Für die vier häufigsten Bewegungen kannst Du Dir merken:

n    Step Over
s    Step Into
r    bis zum Return
c    Continue

Breakpoints direkt in pdb setzen

Du musst nicht für jeden Breakpoint:

breakpoint()

in die Datei schreiben.

Im pdb-Prompt kannst Du mit b einen Breakpoint setzen.

In der aktuellen Datei:

(Pdb) b 42

In einer anderen Datei:

(Pdb) b dungeon.py:150

Oder direkt an einer Funktion:

(Pdb) b verarbeite_befehl

Danach:

(Pdb) c

und pdb hält wieder an, sobald diese Stelle erreicht wird.

Mit:

(Pdb) b

listest Du die vorhandenen Breakpoints auf.

Jeder Breakpoint besitzt eine Nummer.

Entfernen kannst Du beispielsweise Breakpoint 1 mit:

(Pdb) clear 1

Conditional Breakpoints

Besonders nützlich werden Breakpoints in Schleifen oder häufig aufgerufenen Funktionen.

Angenommen, Du willst nur bei der Fackel anhalten:

(Pdb) b dungeon.py:150, argument == "fackel"

Der Breakpoint wird zwar bei Zeile 150 geprüft, hält aber nur an, wenn:

argument == "fackel"

wahr ist.

Die Bedingung eines bestehenden Breakpoints kannst Du ändern:

(Pdb) condition 1 argument == "schluessel"

Oder wieder entfernen:

(Pdb) condition 1

So musst Du nicht fünfzig Schleifendurchläufe mit c überspringen, bis endlich der interessante Wert auftaucht.

display: einen Ausdruck beobachten

Wenn Du denselben Wert nach jedem Schritt erneut eingibst:

(Pdb) p spieler.hp

kann display bequemer sein:

(Pdb) display spieler.hp

pdb merkt sich den Ausdruck für den aktuellen Frame.

Sobald die Ausführung dort wieder anhält und sich das Ergebnis geändert hat, zeigt pdb den neuen Wert automatisch an.

Zum Beispiel:

(Pdb) display hp
display hp: 100
(Pdb) n
display hp: 85  [old: 100]

Alle beobachteten Ausdrücke des aktuellen Frames zeigt:

(Pdb) display

Entfernen kannst Du einen Ausdruck mit:

(Pdb) undisplay hp

Eine Besonderheit gibt es bei mutablen Objekten.

Bei:

(Pdb) display inventar

kann pdb eine Änderung innerhalb derselben Liste übersehen, weil alter und neuer Wert auf dasselbe inzwischen veränderte Objekt zeigen.

Dann kann beispielsweise eine Kopie als Ausdruck helfen:

(Pdb) display inventar[:]

until: mehrere Zeilen überspringen

Zwischen next und continue gibt es noch:

until

Ohne Argument läuft pdb weiter, bis es in der aktuellen Funktion eine spätere Quellcodezeile erreicht oder die Funktion zurückkehrt:

(Pdb) until

Du kannst auch eine Zeilennummer angeben:

(Pdb) until 120

Damit eignet sich until gut, wenn Du innerhalb derselben Funktion einen größeren Block überspringen möchtest, ohne gleich bis zum nächsten beliebigen Breakpoint weiterzulaufen.

jump: den Kontrollfluss verändern

Mit:

(Pdb) jump 120

kannst Du festlegen, welche Zeile im aktuellen untersten Frame als Nächstes ausgeführt werden soll.

Damit kannst Du Code überspringen oder noch einmal ausführen.

Das klingt praktisch, verändert aber den tatsächlichen Programmablauf erheblich.

Nicht jeder Sprung ist erlaubt. Du kannst beispielsweise nicht beliebig in Schleifen oder aus bestimmten finally-Blöcken springen.

Für normale Fehlersuche brauchst Du jump selten. Es ist eher ein Werkzeug für gezielte Experimente im Debugger.

Ein Fehler im Dungeon

Nehmen wir an, der Befehl:

nimm fackel

funktioniert nicht.

Der relevante Code sieht vereinfacht so aus:

def verarbeite_befehl(
    verb,
    argument,
    spieler,
    raum,
    raeume,
    besuchte_raeume,
):
    if verb == "nimm":
        if not argument:
            raise UngueltigerBefehlError(
                "Was möchtest Du nehmen?"
            )

        gegenstand = raum.nimm_gegenstand(argument)
        spieler.nimm(gegenstand)

        print(f"Du nimmst {gegenstand}.")
        print(gegenstand.untersuche())

        return None

Setze den Breakpoint direkt vor die verdächtige Operation:

        breakpoint()
        gegenstand = raum.nimm_gegenstand(argument)

Starte den Dungeon und gib ein:

nimm fackel

Jetzt kannst Du den State prüfen:

(Pdb) p verb
'nimm'
(Pdb) p argument
'fackel'
(Pdb) p raum.name
'Halle'
(Pdb) pp raum.gegenstaende
[Gegenstand(name='Fackel', beschreibung='Eine rußige, aber noch brauchbare Fackel.')]

Bis hierhin sieht alles plausibel aus.

Also gehen wir in die nächste Methode:

(Pdb) s

Dort können wir weiter prüfen:

(Pdb) p name
'fackel'
(Pdb) n
(Pdb) p gesuchter_name
'fackel'

Und innerhalb der Schleife:

(Pdb) p gegenstand.name
'Fackel'
(Pdb) p gegenstand.name.lower()
'fackel'

Der wichtige Punkt ist nicht, möglichst viele Werte auszudrucken.

Du suchst den Übergang:

State entspricht meiner Erwartung
        ↓
irgendeine Operation
        ↓
State entspricht ihr nicht mehr

Je näher Du diesen Übergang eingrenzt, desto näher bist Du an der eigentlichen Ursache.

Post-mortem-Debugging

Manchmal ist das Programm bereits abgestürzt, bevor Du einen Breakpoint setzen konntest.

pdb kann auch einen vorhandenen Traceback untersuchen.

Nach einem Fehler in der REPL

In einer interaktiven Python-Session:

def teile(a, b):
    return a / b

Dann:

teile(10, 0)

Python zeigt den Traceback.

Direkt danach:

import pdb

pdb.pm()

pdb öffnet den Frame der letzten unbehandelten Exception.

Jetzt:

(Pdb) p a
10
(Pdb) p b
0

Du untersuchst damit den Zustand zum Zeitpunkt des Fehlers, obwohl der eigentliche Funktionsaufruf bereits beendet ist.

post_mortem()

Du kannst Post-mortem-Debugging auch ausdrücklich verwenden:

import pdb
import traceback


try:
    main()
except Exception as fehler:
    traceback.print_exc()
    pdb.post_mortem(fehler)

Seit Python 3.13 kann post_mortem() direkt ein Exception-Objekt entgegennehmen.

Innerhalb eines aktiven except-Blocks funktioniert in aktuellen Python-Versionen auch:

pdb.post_mortem()

ohne explizites Argument.

Der pauschale except Exception im Beispiel ist bewusst nur für lokale Fehlersuche gedacht. In produktivem Code solltest Du Fehler nicht einfach abfangen, um anschließend auf interaktive Debugger-Eingaben zu warten.

Verkettete Exceptions untersuchen

Seit Python 3.13 kann pdb beim Post-mortem-Debugging auch durch eine Exception-Chain navigieren.

Zum Beispiel bei:

try:
    lade_datei()
except OSError as fehler:
    raise SpielstandError(
        "Spielstand konnte nicht geladen werden"
    ) from fehler

Im Post-mortem-Debugger zeigt:

(Pdb) exceptions

die verketteten Exceptions.

Mit beispielsweise:

(Pdb) exceptions 0

kannst Du zu einer davon wechseln und anschließend deren Frames untersuchen.

Das ist besonders praktisch bei sauberer Exception-Verkettung mit:

raise ... from fehler

weil Du sowohl den Fehler auf Anwendungsebene als auch seine ursprüngliche Ursache untersuchen kannst.

Ein Programm direkt unter pdb starten

Statt einen breakpoint() in den Quellcode einzubauen, kannst Du das gesamte Skript unter pdb starten:

python -m pdb dungeon.py

pdb hält vor der ersten auszuführenden Zeile.

Jetzt kannst Du beispielsweise einen Breakpoint setzen:

(Pdb) b verarbeite_befehl
(Pdb) c

Bei einer unbehandelten Exception wechselt der über die Kommandozeile gestartete Debugger automatisch ins Post-mortem-Debugging.

Nach dem normalen Ende oder nach der Post-mortem-Sitzung startet pdb das Programm erneut. Dabei bleiben unter anderem gesetzte Breakpoints erhalten.

Beenden kannst Du die Debugger-Sitzung mit:

(Pdb) q

Ein Modul mit pdb starten

Falls Dein Programm normalerweise als Modul ausgeführt wird:

python -m dungeon

kann pdb ebenfalls ein Modul starten:

python -m pdb -m dungeon

Das ist gerade bei Packages besser, als künstlich eine interne Datei direkt auszuführen und damit möglicherweise das normale Import-Verhalten zu verändern.

Mit -c Debugger-Befehle beim Start ausführen

Der pdb-CLI kannst Du Befehle direkt mitgeben:

python -m pdb -c continue dungeon.py

pdb führt beim Start sofort continue aus.

Das Programm läuft damit zunächst normal weiter und bleibt trotzdem unter Debugger-Kontrolle.

Oder Du setzt direkt einen Breakpoint:

python -m pdb -c "break verarbeite_befehl" dungeon.py

Mehrere -c-Optionen sind ebenfalls möglich.

Das ist praktisch, wenn Du den Quellcode nicht verändern möchtest.

Neu in Python 3.14: an einen laufenden Prozess anhängen

Seit Python 3.14 kann pdb sich auch an einen bereits laufenden CPython-Prozess hängen.

Du brauchst dafür dessen Process ID:

python -m pdb -p 1234

Das ist interessant, wenn ein lokaler Prozess bereits läuft und sich erst dann ungewöhnlich verhält.

Du musst also nicht zwingend vorher:

breakpoint()

eingebaut oder das Programm unter:

python -m pdb

gestartet haben.

Der Prozess muss Python-Code ausführen können

Das Anhängen kann nicht jeden blockierten Prozess augenblicklich aus einem beliebigen System Call herausreißen.

Wartet der Prozess beispielsweise gerade blockierend auf I/O, wird der Debugger unter Umständen erst aktiv, sobald wieder Python-Bytecode ausgeführt wird oder der Prozess ein Signal erhält.

Betriebssystemrechte gelten weiterhin

An einen fremden Prozess darf sich nicht jeder Benutzer beliebig anhängen.

Je nach Betriebssystem werden entsprechende Debugging-Rechte benötigt. Unter Linux spielen beispielsweise ptrace-Berechtigungen und gegebenenfalls Security-Einstellungen wie Yama eine Rolle.

In Containern kann ebenfalls eine zusätzliche Berechtigung erforderlich sein.

Diese Schutzmechanismen sollte man nicht leichtfertig global abschalten. Die Möglichkeit, Code in einen laufenden Prozess einzuschleusen, ist offensichtlich sicherheitsrelevant.

Die Remote-Debugging-Funktion lässt sich beim Start eines CPython-Prozesses außerdem deaktivieren, etwa mit:

python -X disable-remote-debug app.py

oder über:

PYTHON_DISABLE_REMOTE_DEBUG=1

breakpoint() lässt sich konfigurieren

breakpoint() ist nicht fest mit pdb verdrahtet.

Intern ruft die Funktion:

sys.breakpointhook()

auf.

Die Standardimplementierung verwendet normalerweise:

pdb.set_trace()

Über die Environment Variable PYTHONBREAKPOINT kannst Du dieses Verhalten ändern.

Breakpoints für einen Lauf deaktivieren

Mit:

PYTHONBREAKPOINT=0

wird der Standard-Breakpoint-Hook praktisch zum No-op.

Unter Linux oder macOS:

PYTHONBREAKPOINT=0 python dungeon.py

In PowerShell:

$env:PYTHONBREAKPOINT = "0"
python dungeon.py

In cmd.exe:

set PYTHONBREAKPOINT=0
python dungeon.py

Das ist nützlich, wenn absichtlich vorhandene breakpoint()-Aufrufe für einen bestimmten Lauf ignoriert werden sollen.

Ein versehentlicher Breakpoint sollte vor einem Deployment trotzdem nicht darauf angewiesen sein, dass irgendwo hoffentlich die richtige Environment Variable gesetzt wurde.

Einen anderen Debugger verwenden

PYTHONBREAKPOINT kann auf einen anderen Callable zeigen.

Wenn beispielsweise ipdb installiert ist:

PYTHONBREAKPOINT=ipdb.set_trace python dungeon.py

Der Quellcode bleibt:

breakpoint()

Dadurch ist:

breakpoint()

flexibler als ein hart eingebautes:

import pdb

pdb.set_trace()

Die Anwendung markiert nur:

Hier möchte ich in den konfigurierten Debugger wechseln.

Welcher Debugger das tatsächlich ist, kann die Laufzeitumgebung entscheiden.

Python 3.14 und asyncio

Ein normaler Breakpoint kann auch in asynchronem Code anhalten. Wenn Du im Debugger selbst aber mit await arbeiten möchtest, gibt es seit Python 3.14 eine speziell dafür vorgesehene Variante:

import pdb


async def verarbeite():
    daten = await lade_daten()

    await pdb.set_trace_async()

    return daten

set_trace_async() wird innerhalb einer Coroutine mit await aufgerufen.

In einer so gestarteten pdb-Sitzung kannst Du auch Await-Ausdrücke verwenden.

Das ist beispielsweise interessant, wenn Du im angehaltenen Async-Code eine weitere Coroutine untersuchen möchtest.

Python 3.14 stellt in pdb außerdem:

$_asynctask

bereit, um auf den aktuellen asyncio-Task zuzugreifen, sofern einer vorhanden ist.

Für normalen synchronen Code bleibt:

breakpoint()

die einfachste Variante.

Debugging und Nebenläufigkeit

Ein interaktiver Debugger verändert das Timing eines Programms erheblich.

Das ist besonders wichtig bei:

Threads
asyncio
Multiprocessing
Locks
Timeouts
Netzwerkkommunikation

Wenn Du an einem Breakpoint mehrere Minuten stehen bleibst, wartet die Außenwelt nicht unbedingt mit.

Ein Netzwerk-Timeout kann ablaufen, ein anderer Thread kann State verändern oder ein externer Dienst kann eine Verbindung schließen.

Bei Race Conditions kann der Debugger das Problem sogar verschwinden lassen, weil das Programm plötzlich vollkommen anders getaktet wird.

pdb ist deshalb hervorragend, um State und Kontrollfluss zu verstehen. Für reine Timing-Probleme brauchst Du häufig zusätzlich Logging, reproduzierbare Tests oder spezialisierte Werkzeuge.

pdb oder Logging?

Beide Werkzeuge beantworten unterschiedliche Fragen.

pdb

pdb ist interaktiv.

Du willst jetzt herausfinden:

Warum hat diese Variable diesen Wert?
Welche Zeile verändert den State?
Warum läuft der Code in diesen Branch?
Was liefert dieser Funktionsaufruf zurück?
Wie sieht der Stack an dieser Stelle aus?

Der Debugger hält die Ausführung dafür an.

Logging

Logging sammelt Informationen während das Programm normal läuft.

Du möchtest später wissen:

Wann wurde der Prozess gestartet?
Welche Datei wurde geladen?
Welche Operation ist fehlgeschlagen?
Welche Requests liefen vorher?
Was passierte kurz vor einer Exception?

Für Cronjobs, Server, Bots und Probleme auf fremden Systemen ist Logging deshalb oft wichtiger.

Sehr verkürzt:

pdb       Was passiert hier gerade?
logging   Was ist über die Zeit passiert?

In der Praxis verwendet man häufig beides.

pdb und IDE-Debugger

Visual Studio Code, PyCharm und andere Entwicklungsumgebungen bieten grafische Debugger.

Dort klickst Du Breakpoints an eine Zeile, siehst Variablen in Panels und navigierst grafisch durch den Call Stack.

pdb wirkt daneben zunächst spartanisch.

Es hat aber einen großen Vorteil: Es gehört zu Python.

Damit funktioniert es auch:

  • in einem Terminal,
  • per SSH auf einem Server,
  • in kleinen Skripten,
  • in einer minimalen Entwicklungsumgebung,
  • wenn keine IDE zur Verfügung steht.

Die zugrunde liegenden Konzepte sind ohnehin dieselben:

Breakpoint
Step Into
Step Over
Continue
Stack Frame
Conditional Breakpoint
Post Mortem

Wer diese Begriffe mit pdb verstanden hat, findet sich auch in einem grafischen Debugger schnell zurecht.

Ein brauchbarer Debugging-Ablauf

Ein Debugger ist am hilfreichsten, wenn Du nicht völlig wahllos durch den Code steppst.

Ein besserer Ablauf beginnt mit einer konkreten Frage.

1. Fehler reproduzieren

Finde eine Eingabe oder einen Ablauf, der das Problem möglichst zuverlässig auslöst.

nimm fackel

2. Erwartung formulieren

Bevor Du einen Wert ansiehst, frage Dich:

Was müsste hier eigentlich stehen?

Zum Beispiel:

argument müsste "fackel" sein

3. Kurz vor der verdächtigen Stelle anhalten

breakpoint()

gegenstand = raum.nimm_gegenstand(argument)

4. State überprüfen

(Pdb) p argument
(Pdb) pp raum.gegenstaende

5. Den falschen Übergang suchen

Wenn der State noch stimmt:

(Pdb) n

oder:

(Pdb) s

bis Du die erste Stelle findest, an der Realität und Erwartung auseinandergehen.

6. Ursache beheben

Nicht einfach den falschen Wert unmittelbar vor seinem Einsatz korrigieren, sondern herausfinden, warum er überhaupt falsch geworden ist.

7. Regression absichern

Wenn möglich, schreibe anschließend einen Test, der genau diesen Fehler reproduziert.

Dann wird aus:

Ich habe den Bug gefunden.

ein:

Dieser Bug soll nicht noch einmal zurückkommen.

Häufige Stolperfallen

  • breakpoint() versehentlich im produktiven Code aktiv lassen: Ein erreichter Breakpoint kann einen Prozess auf interaktive Eingabe warten lassen.

  • n und s verwechseln: next bleibt im aktuellen Frame, step kann in einen aufgerufenen Frame wechseln.

  • Mit step tief in Bibliothekscode geraten: Meist wolltest Du eigentlich nur Deine eigene Funktion untersuchen. Mit return oder continue kommst Du wieder heraus.

  • Im Debugger State ändern und es anschließend vergessen: Zuweisungen und Methodenaufrufe verändern das tatsächlich laufende Programm.

  • Eine Debugger-Abfrage für nebenwirkungsfrei halten: Auch p objekt.methode() kann Code ausführen und State verändern.

  • Variablennamen mit pdb-Kommandos verwechseln: Bei echten Kollisionen kannst Du Python-Code mit ! erzwingen.

  • continue für „eine Zeile weiter“ halten: c läuft bis zum nächsten Breakpoint oder bis zum Programmende. Für einzelne Schritte gibt es n und s.

  • jump unbedacht verwenden: Damit veränderst Du den normalen Kontrollfluss und kannst einen Zustand erzeugen, den das Programm sonst nie erreichen würde.

  • Bei einem Crash sofort neu starten: Post-mortem-Debugging kann den State des fehlerhaften Frames noch zugänglich machen.

  • Post-mortem-Hooks in produktivem Code lassen: Ein Server sollte nach einem Fehler nicht auf einen Menschen am pdb-Prompt warten.

  • PYTHONBREAKPOINT=0 als Ersatz fürs Aufräumen verwenden: Absichtlich konfigurierbare Breakpoints sind etwas anderes als versehentlich vergessene.

  • Einen laufenden Prozess mit -p attachen und Sicherheitsmechanismen dafür global abschalten: Debugging-Rechte sind ein Sicherheitsfeature.

  • Nebenläufigen Code wie synchronen Code behandeln: Ein Breakpoint verändert Timing und kann Race Conditions beeinflussen.

  • Alles mit pdb lösen wollen: Probleme, die nur in Produktion oder selten auftreten, brauchen häufig gutes Logging und reproduzierbare Tests.

  • Sensible Daten anzeigen: Im Debugger kannst Du genauso leicht Passwörter, Tokens oder personenbezogene Informationen sehen wie in einem Log.

Kompakte Übersicht

Ziel Befehl
Breakpoint im Code breakpoint()
Skript unter pdb starten python -m pdb skript.py
Modul unter pdb starten python -m pdb -m paket.modul
an Prozess anhängen, Python 3.14+ python -m pdb -p PID
Breakpoints deaktivieren PYTHONBREAKPOINT=0
Hilfe h
Quellcode l oder ll
Step Over n
Step Into s
bis zum Return r
Continue c
Ausdruck ausgeben p ausdruck
Pretty Print pp ausdruck
Argumente anzeigen a
Call Stack anzeigen w
im Stack nach oben u
im Stack nach unten d
Breakpoint setzen b zeile oder b funktion
Conditional Breakpoint b zeile, bedingung
Ausdruck beobachten display ausdruck
Beobachtung entfernen undisplay ausdruck
bis zu späterer Zeile laufen until oder until zeile
Return Value anzeigen retval
Ausführungsposition ändern jump zeile
Exception Chain anzeigen exceptions
Debugger verlassen q

Übungen

1. Einen Breakpoint setzen

Lege debug_uebung.py an:

def verdopple(zahl):
    ergebnis = zahl * 2

    breakpoint()

    return ergebnis


wert = verdopple(21)

print(wert)

Starte die Datei und ermittle im Debugger:

  • zahl
  • ergebnis
  • zahl + ergebnis
Lösung
python debug_uebung.py
Im Debugger:
(Pdb) p zahl
21
(Pdb) p ergebnis
42
(Pdb) p zahl + ergebnis
63
(Pdb) c
Anschließend gibt das Programm:
42
aus.

2. next und step vergleichen

Verwende:

def berechne_bonus(level):
    return level * 10


def berechne_gold(level):
    basis = 50
    bonus = berechne_bonus(level)

    return basis + bonus


breakpoint()

gold = berechne_gold(3)

print(gold)

Gehe einmal mit n und einmal nach einem Neustart mit s über:

gold = berechne_gold(3)
Lösung Mit:
(Pdb) n
wird `berechne_gold(3)` ausgeführt, ohne dass Du die Funktion einzeln durchläufst. Mit:
(Pdb) s
steigst Du in `berechne_gold()` hinein. Von dort kannst Du später auch in:
berechne_bonus(level)
hineinsteppen.

3. Den Stack untersuchen

Nutze:

def innen(x):
    breakpoint()

    return 10 / x


def mitte(x):
    return innen(x)


def aussen(x):
    return mitte(x)


print(aussen(2))

Zeige im Debugger den Call Stack und wechsle anschließend zum Aufrufer.

Lösung
(Pdb) w
(Pdb) u
(Pdb) a
`w` zeigt die Frames. `u` wechselt zum Aufrufer. `a` zeigt die Argumente dieses Frames. Mit:
(Pdb) d
gehst Du wieder in Richtung des jüngeren Frames.

4. Post-mortem-Debugging

Starte eine Python-REPL:

def teile(a, b):
    return a / b


teile(10, 0)

Nach dem Traceback:

import pdb

pdb.pm()

Finde die Werte von a und b.

Lösung
(Pdb) p a
10
(Pdb) p b
0
(Pdb) q
`pdb.pm()` öffnet den Debugger am letzten unbehandelten Fehler.

5. Einen Conditional Breakpoint verwenden

Nutze:

def verarbeite(werte):
    for wert in werte:
        ergebnis = 100 / wert
        print(ergebnis)


verarbeite([5, 4, 2, 1])

Starte das Skript mit:

python -m pdb debug_uebung.py

Setze einen Breakpoint auf die Division, der nur für:

wert == 2

anhält.

Lösung Wenn die Division beispielsweise in Zeile 3 steht:
(Pdb) b 3, wert == 2
(Pdb) c
pdb hält erst bei dem Schleifendurchlauf an, in dem `wert` tatsächlich `2` enthält. Die konkrete Zeilennummer musst Du an Deine Datei anpassen.

Weiterlesen

0 Kommentare

Noch keine Kommentare. Sei der/die Erste!