The pure-Python pickle._Unpickler mishandles a constructor failure while unpickling old-style instances (the INST and OBJ opcodes). If the class constructor raises TypeError, _instantiate does:
raise TypeError("in constructor for %s: %s" % (klass.__name__, str(err)), err.__traceback__)
Passing err.__traceback__ as a second positional argument puts the traceback object into the exception's args, so it leaks into str(), and there is no real chaining (__cause__ stays None).
import pickle, io class Bad: def __init__(self, *a): raise TypeError("boom") u = pickle._Unpickler(io.BytesIO()) try: u._instantiate(Bad, (1,)) except TypeError as e: print(e.args) # ('in constructor for Bad: boom', <traceback object ...>) print(e.__cause__) # None
The C _pickle unpickler does not wrap the error at all, it just lets the original TypeError propagate, so this only affects the pure-Python implementation.
The fix is to drop the wrapper and let the original TypeError propagate:
value = klass(*args)
The wrapper was added in 743d17e (1998) to name the class when __getinitargs__ returned something bogus. The constructor error already names the class in that case, so the prefix only repeats it:
in constructor for WrongArgCount: WrongArgCount.__init__() takes 2 positional arguments but 3 were given
The traceback in args is a leftover from the Python 2 way of carrying one. It comes from 26d95c3 , where raise T, V, tb became raise T(V, tb) instead of raise T(V).with_traceback(tb). gh-102799 later changed sys.exc_info()[2] to err.__traceback__ and nothing else.
Found while going through devdanzin's audit of the standard library, item 15: https://gist.github.com/devdanzin/3198710e3c0128fda5e0a7b4e0768e5f