sys.exc_info() - the exception info that's been currently handled. Note that the exception could contain inner exceptions via __context__, or __cause__. And this could be nested for multiple levels.

pdb.post_mortem(traceback) - once found the exception you want to post-mortem debug, use:

1
pdb.post_mortem(exception.__traceback__)

About exception chaining (both implicit and explicit), see PEP-3134.

Explicit chaining is via:

1
2
3
4
5
try:
...
except Exception as e:
raise SomeErr(...) from e
# ~~~~~~ this!

In above case, the SomeErr instance contains __cause__ which is e.

Implicit chaining:

1
2
3
4
5
try:
...
except Exception as e:
raise SomeErr(...)
# ~~~~~~~~ implicit chaining (by interpreter)

In implicit chaining, interpreter automatically sets __context__ on the new exception instance SomeErr. TBH, I feel the differentiation of implicit and explicit chaining via __cause__ and __context__ is unnecessary. And it causes extra complexity to handle them.

PDB's handling of inner exceptions

...there is none. PDB's post mortem pdb.pm() doesn't debug the inner-most exception of sys.last_value. In fact, sys.last_value isn't always present. So pm() is not a reliable way of post-mortem debugging.

To debug the inner-most exception:

1
2
3
4
5
6
7
import sys, pdb
def debug_inner():
'''Debug the inner-most exception's traceback'''
exc = sys.exc_info()[1]
while getattr(exc, '__context__', None):
exc = exc.__context__
pdb.post_mortem(exc.__traceback__)

I created a .pdbrc alias for this:

1
2
3
4
5
6
7
8
9
10
_LOCAL = dict()
_DEF = ""
_DEF += "\ndef debug_inner():"
_DEF += "\n '''Debug the inner-most exception's traceback'''"
_DEF += "\n exc = __import__('sys').exc_info()[1]"
_DEF += "\n while getattr(exc, '__context__', None):"
_DEF += "\n exc = exc.__context__"
_DEF += "\n __import__('pdb').post_mortem(exc.__traceback__)"
exec(_DEF, dict(), _LOCAL)
alias di _LOCAL['debug_inner']()