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:
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
Linked PRs
The pure-Python
pickle._Unpicklermishandles a constructor failure while unpickling old-style instances (the INST and OBJ opcodes). If the class constructor raisesTypeError,_instantiatedoes:Passing
err.__traceback__as a second positional argument puts the traceback object into the exception'sargs, so it leaks intostr(), and there is no real chaining (__cause__staysNone).The C
_pickleunpickler does not wrap the error at all, it just lets the originalTypeErrorpropagate, so this only affects the pure-Python implementation.The fix is to drop the wrapper and let the original
TypeErrorpropagate: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:
The traceback in
argsis a leftover from the Python 2 way of carrying one. It comes from26d95c3, where
raise T, V, tbbecameraise T(V, tb)instead ofraise T(V).with_traceback(tb). gh-102799 later changedsys.exc_info()[2]toerr.__traceback__and nothing else.Found while going through devdanzin's audit of the standard library, item 15: https://gist.github.com/devdanzin/3198710e3c0128fda5e0a7b4e0768e5f
Linked PRs