How do you drop into pdb on the traceback of an exception you have already caught?
answer
- The exception carries its own stack
- One attribute holds the frame chain
- A pdb function takes that chain
- A no-argument shortcut for the REPL
- post_mortem, not set_trace, in a handler
basics
~10 sCall pdb.post_mortem() with the caught exception's traceback: pdb.post_mortem(exc.__traceback__), or on 3.13+ pass the exception itself. You land in the frame that raised, with its local variables readable but not resumable.
solid answer
~40 sEvery exception instance carries the stack it travelled through on its `__traceback__` attribute, and `pdb.post_mortem()` takes exactly that and opens a debugger prompt in the innermost frame of it. So inside a handler you write `pdb.post_mortem(exc.__traceback__)`; since 3.13 `pdb.post_mortem(exc)` works too, and `sys.exc_info()[2]` gives the same traceback for the exception currently being handled. In an interactive session there is a no-argument shortcut, `pdb.pm()`, which reuses the exception the interpreter recorded from the last unhandled error - the one the REPL just printed - so you can crash, then inspect. In all of these the frames are already unwound: you read state and move `up`/`down` the captured chain, you do not continue execution.
code
python · 15 linesimport os
import pdb
def settle(order):
return order[2]
try:
settle(["impression", "click"])
except IndexError as exc:
if os.isatty(0):
pdb.post_mortem(exc)
else:
print("no terminal; skipping post-mortem")go deeper
Remember that a caught exception carries its stack on __traceback__, and that pdb has a function which takes it and opens a prompt where the error happened. Knowing the name pdb.post_mortem is most of the answer at this level.
Explain the mechanics: traceback objects hold frames, post_mortem walks to the innermost one, sys.exc_info() exposes the same traceback inside a handler, and pdb.pm() is the REPL shortcut. Be clear that nothing resumes.
Demonstrate the guards you would put around it in real code - a tty check and an explicit opt-in, re-raising afterwards - and be able to say why set_trace in a handler is the wrong reflex and which frame in the chain is usually the informative one.
Take a position on whether interactive post-mortem belongs anywhere near production at all, and on what a team should standardise instead so that a failure is diagnosable from what it recorded rather than from someone's terminal.
### The traceback is an object, not text The multi-line block Python prints on a crash is a *rendering*. The real thing is a linked chain of traceback objects, each pointing at a frame object, and the chain hangs off the exception instance as `__traceback__`. Because those traceback objects hold the frames, the frames' local variables stay alive as long as you hold the exception - which is precisely what makes deferred inspection possible. So 'entering post-mortem' is not magic. It is: take that chain, walk to its innermost end, and point a debugger at the frame there. ### The three entry points **`pdb.post_mortem(t)`** is the general one. Classically `t` is a traceback object, so the form that works on every version in service is `pdb.post_mortem(exc.__traceback__)` inside the handler. Since 3.13 it also accepts the exception instance itself, so `pdb.post_mortem(exc)` reads better on current interpreters. With no argument at all it uses the exception currently being handled. **`sys.exc_info()`** returns the `(type, value, traceback)` triple for the exception being handled right now, anywhere in the dynamic extent of the handler - including inside a helper the handler called, which is why a logging or reporting utility can recover the traceback without having it passed in. Outside any handler it returns `(None, None, None)`. Since 3.11 `sys.exception()` returns just the instance, and `exc.__traceback__` gets you the third element from it. **`pdb.pm()`** is the interactive shortcut with no arguments: it reuses the exception the interpreter recorded for the *last unhandled* error in the session. The workflow is the reason the function exists - run something in the REPL, watch it blow up, type `import pdb; pdb.pm()`, and you are standing in the frame that raised, without having predicted the failure or re-run anything. ### What you can and cannot do at that prompt You can print names in the raising frame, evaluate expressions against them, and move along the captured chain to a caller and print its names. That is usually enough: the question a post-mortem answers is 'what were the values when it broke?', and the answer to 'why did the index go out of range' is almost always visible in the frame's own arguments. What you cannot do is continue. The handler already ran; those frames are unwound and the interpreter is executing your `except` block, not them. Rebinding a name at the post-mortem prompt changes the dead frame's mapping and affects nothing. ### The pattern in real code ```python import os import pdb try: run_pipeline() except Exception as exc: if os.isatty(0) and os.environ.get("DEBUG_PM"): pdb.post_mortem(exc) raise ``` Two guards matter. First, a terminal: `pdb.post_mortem` reads commands from standard input, so in a service, a scheduled job or a container with no tty it blocks or dies confusingly instead of helping. Second, an opt-in switch, so the debugger never appears in an unattended run. Re-raising afterwards keeps the program's normal failure semantics intact - the debugger is an observer, not an error handler. ### Where it goes wrong A frequent mistake is calling `pdb.set_trace()` in the handler instead. That does open a prompt, but in the *handler's* frame: you see the exception variable and whatever else the handler has, and the state that actually caused the failure - the raising frame's arguments and locals - is not in scope. `post_mortem` is what walks you back into that frame. A second mistake is holding the exception past the handler and expecting the name to still be bound; the `except ... as` name is deleted when the clause ends, so a deferred post-mortem needs the exception (or its traceback) copied to another name first. A third mistake is assuming the innermost frame is always the interesting one. It often is, but when the raise happens inside a library called with bad arguments, the informative frame is one or two levels out, which is why navigating the captured stack is part of the technique rather than an afterthought. ### Pointing the debugger somewhere other than the terminal `pdb.post_mortem()` uses a default debugger instance that reads commands from standard input and writes to standard output. When those are not what you want - a wrapper script, a test harness, a custom console - you construct `pdb.Pdb` yourself with the streams you want and call its own post-mortem entry point on the traceback. That is also the honest answer to 'can I post-mortem in a service?': not with the default instance, because there is no terminal on the other end of standard input.
- Why is `pdb.set_trace()` inside the `except` block not the same thing?`pdb.set_trace()` opens a prompt in the frame that is currently executing - the handler. From there you can see the exception object and the handler's own names, but the raising function's arguments and locals are not in scope; that frame is only reachable through the traceback. `pdb.post_mortem()` follows the traceback into it, which is the state you actually wanted.
- How would you reach the caller's variables rather than the raising frame's?Post-mortem drops you at the innermost end of the captured chain, but the whole chain is there: `up` moves your view one frame outward, `down` moves back in, and each move re-scopes name lookups to that frame. When a library raises because it was handed bad arguments, the frame worth reading is the one or two levels out that built them.
- What does `sys.exc_info()` return when called outside any exception handler?`(None, None, None)`. It reports the exception *currently being handled*, not the last one that ever occurred, and that state is per-thread and unwound when the handler ends. Code that expects to fish out a traceback long after the fact must have captured the exception object itself; asking `sys.exc_info()` later just gets three `None`s.
saying these in an interview costs you the question
- Uses `pdb.set_trace()` in the handler and expects the raising frame
- Thinks the traceback is only a printable string
- Believes post-mortem can resume the failed call
- Expects `sys.exc_info()` to work outside a handler
- Leaves an unguarded `pdb.post_mortem()` in service code
- Confuses the exception object with its traceback object