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. -
nundsverwechseln:nextbleibt im aktuellen Frame,stepkann in einen aufgerufenen Frame wechseln. -
Mit
steptief in Bibliothekscode geraten: Meist wolltest Du eigentlich nur Deine eigene Funktion untersuchen. Mitreturnodercontinuekommst 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. -
continuefür „eine Zeile weiter“ halten:cläuft bis zum nächsten Breakpoint oder bis zum Programmende. Für einzelne Schritte gibt esnunds. -
jumpunbedacht 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=0als Ersatz fürs Aufräumen verwenden: Absichtlich konfigurierbare Breakpoints sind etwas anderes als versehentlich vergessene. -
Einen laufenden Prozess mit
-pattachen 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:
zahlergebniszahl + ergebnis
Lösung
python debug_uebung.py
(Pdb) p zahl
21
(Pdb) p ergebnis
42
(Pdb) p zahl + ergebnis
63
(Pdb) c
42
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
(Pdb) s
berechne_bonus(level)
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
(Pdb) d
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
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
Weiterlesen
- Python lernen, Teil 8: Wenn's schiefgeht
- Logging statt print
- Python-Dokumentation:
pdb– The Python Debugger - Python-Dokumentation:
breakpoint() - Python-Dokumentation:
PYTHONBREAKPOINT - Python-Dokumentation: Remote Debugging
- PEP 553: Built-in
breakpoint() - PEP 667: Consistent views of namespaces
- PEP 768: Safe external debugger interface for CPython
0 Kommentare
Noch keine Kommentare. Sei der/die Erste!
Anmelden um einen Kommentar zu hinterlassen.