A batch job crashes once at 3 a.m. with a KeyError deep in a call stack, and you cannot reproduce it interactively. Re-running with a breakpoint is hopeless if the input is gone. Post-mortem debugging reopens the exact frame where the exception was raised, with every local still intact, so you inspect the state at the moment of failure rather than guessing from a traceback.
Prerequisites
- Python 3.x —
pdb.post_mortem,pdb.pm, and thesys.last_*attributes are long-standing standard-library features. - pytest 5.4+ for
--pdbpost-mortem on test failure. - IPython if you want the
%debugmagic; the underlying mechanism is identical. - The command vocabulary from interactive debugging with pdb and ipdb — post-mortem drops you into the same prompt.
Solution
When an unhandled exception reaches the top level, the interpreter stores its traceback on sys.last_traceback (with sys.last_value and sys.last_type). pdb.pm() opens a post-mortem session on that stored traceback; pdb.post_mortem(tb) does the same for any traceback object you hand it.
# In an interactive session or REPL after a crash:
import pdb
def load(config):
return config["timeout"] # KeyError if the key is missing
load({}) # raises KeyError: 'timeout', prints a traceback
pdb.pm() # reopen the failing frame from sys.last_traceback
> example.py(4)load()
-> return config["timeout"]
(Pdb) p config # the exact argument that caused the crash, still alive
{}
(Pdb) up # walk toward the caller if needed
(Pdb) p sys.last_value
KeyError('timeout')
When you catch the exception yourself, grab the traceback off the exception object and pass it explicitly — this works inside a script where sys.last_traceback is never set:
import pdb
import sys
def run():
try:
risky()
except Exception:
# __traceback__ holds the frames; post_mortem reopens the deepest one
pdb.post_mortem(sys.exc_info()[2])
def risky():
data = [1, 2, 3]
return data[99] # IndexError
For an unattended script, launch it under pdb with -c continue. The script runs at full speed; only if it crashes does pdb take over at the failing frame:
python -m pdb -c continue batch_job.py
# ... normal output ...
# Traceback ... then automatically:
# > batch_job.py(57)transform()
# -> return row[key]
# (Pdb)
Inside IPython or Jupyter, %debug is the one-liner equivalent of pdb.pm() — it opens a post-mortem prompt on the last exception. %pdb on arms it so every subsequent uncaught exception drops you in automatically.
# IPython
In [1]: load({}) # raises KeyError
In [2]: %debug # post-mortem prompt at the failing frame
pytest exposes the same behaviour with --pdb: on any test failure it opens a post-mortem prompt in the failing frame with the assertion's exception live.
pytest tests/test_config.py --pdb # drop into post-mortem at the point of failure
This is the natural follow-on to --trace (break at test start) covered in the parent guide; use --pdb when you want to inspect after the failure rather than step into the test. If the failure is intermittent across runs, pair post-mortem triage with the rerun analysis in debugging flaky tests with pytest-rerunfailures.
Why this works
A traceback object is a linked list of frame objects, each carrying its locals and globals at the instant the exception unwound. Post-mortem debugging does not re-execute anything — it points the pdb command loop at the deepest frame in that captured chain, so p, up, and down read the preserved state. Because the frames are kept alive by the traceback reference, the state survives until that reference is dropped; pdb.pm() simply reuses the interpreter's own top-level capture (sys.last_traceback).
Edge cases and failure modes
pdb.pm()raisesAttributeErrorwhen no unhandled exception has reached the top level — there is nosys.last_traceback. Usepdb.post_mortem(tb)with an explicit traceback inside scripts.- The traceback's frames pin their locals in memory; holding a traceback reference (or an exception via
except ... as e) for a long time can leak large objects — clear it when done. - Post-mortem state is read-only in spirit: you can evaluate expressions, but you cannot resume execution from a post-mortem frame;
continuesimply exits the session. python -m pdb -c continuere-runs the program; if the crash depends on external state that has changed, it may not reproduce.- Chained exceptions (
raise ... from) land you on the most recent one; walksys.last_value.__cause__or__context__to inspect the original.
Frequently Asked Questions
What is the difference between pdb.pm() and pdb.post_mortem()?pdb.post_mortem(tb) starts a post-mortem session on a traceback you pass it. pdb.pm() is a convenience wrapper that calls post_mortem on sys.last_traceback, the traceback of the most recent unhandled exception stored by the interpreter, so you can debug a crash that already printed.
Why is sys.last_traceback sometimes missing?
The interpreter only sets sys.last_traceback, sys.last_value, and sys.last_type when an unhandled exception reaches the top level in interactive mode. Inside a script that caught the exception, or in a fresh process, the attribute does not exist, so pdb.pm() raises AttributeError.
How do I get a post-mortem prompt automatically when a script crashes?
Run python -m pdb -c continue your_script.py. The -c continue runs the script normally, and if it raises an unhandled exception pdb drops into a post-mortem prompt at the failing frame instead of exiting.
← Back to Interactive Debugging with pdb and ipdb