55

Why does my custom Exception class below not serialize/unserialize correctly using the pickle module?

import pickle

class MyException(Exception):
    def __init__(self, arg1, arg2):
        self.arg1 = arg1
        self.arg2 = arg2

        super(MyException, self).__init__(arg1)

e = MyException("foo", "bar")

str = pickle.dumps(e)
obj = pickle.loads(str)

This code throws the following error:

Traceback (most recent call last):
File "test.py", line 13, in <module>
   obj = pickle.loads(str)
File "/usr/lib/python2.7/pickle.py", line 1382, in loads
   return Unpickler(file).load()
File "/usr/lib/python2.7/pickle.py", line 858, in load
   dispatch[key](self)
File "/usr/lib/python2.7/pickle.py", line 1133, in load_reduce
   value = func(*args)
TypeError: __init__() takes exactly 3 arguments (2 given)

I'm sure this problem stems from a lack of knowledge on my part of how to make a class pickle-friendly. Interestingly, this problem doesn't occur when my class doesn't extend Exception.

7 Answers 7

60

The current answers break down if you're using both arguments to construct an error message to pass to the parent Exception class. I believe the best way is to simply override the __reduce__ method in your exception. The __reduce__ method should return a two item tuple. The first item in the tuple is your class. The second item is a tuple containing the arguments to pass to your class's __init__ method.

import pickle

class MyException(Exception):
    def __init__(self, arg1, arg2):
        super().__init__(f'arg1: {arg1}, arg2: {arg2}')
        self.arg1 = arg1
        self.arg2 = arg2
    
    def __reduce__(self):
        return (MyException, (self.arg1, self.arg2))

# Create an exception instance and print info about it
original = MyException('foo', 'bar')
print(repr(original))
print(original.arg1)
print(original.arg2)

# Pickle and unpickle the exception, info printed should match above
reconstituted = pickle.loads(pickle.dumps(original))
print(repr(reconstituted))
print(reconstituted.arg1)
print(reconstituted.arg2)

More info about __reduce__ here.

Sign up to request clarification or add additional context in comments.

12 Comments

This is the best answer. The other two are really just hacks that kind of work. This one builds the class properly.
@borgr Sorry, but I had to revert your edit. Calling super with self.__class__ is a bad practice that can break inheritance. See this gist: gist.github.com/ulope/1935894
@sean what would be the right practice then if some would want to be able to have inheritance? It loses a lot of the power of classes if you can't inherit (or you have to override everything). And also I don't see why in the reduce it is a problem, the error linked only happens because of the dynamically changing super class.
@Sean I don't even have a case :-) I just wondered if it is not a bad practice to create a class with a function (reduce here) with the class name hard coded, so it can not be inherited, is there any disadvantage for not hard coding the class name there?
@borgr, parent classes are inherited by their subclasses, so referring to a parent class in the method is the proper thing to do: class MySubException(MyException): pass. In MySubException, when __reduce__ is called, since MySubException did not override the method, what is called is actually MyException.__reduce__(obj) and this method is referring to the superclass of MyException, i.e. Exception. Long story short: don't worry about it.
|
43

Make arg2 optional:

class MyException(Exception):
    def __init__(self, arg1, arg2=None):
        self.arg1 = arg1
        self.arg2 = arg2
        super(MyException, self).__init__(arg1)

The base Exception class defines a .__reduce__() method to make the extension (C-based) type picklable and that method only expects one argument (which is .args); see the BaseException_reduce() function in the C source.

The easiest work-around is making extra arguments optional. The __reduce__ method also includes any additional object attributes beyond .args and .message and your instances are recreated properly:

>>> e = MyException('foo', 'bar')
>>> e.__reduce__()
(<class '__main__.MyException'>, ('foo',), {'arg1': 'foo', 'arg2': 'bar'})
>>> pickle.loads(pickle.dumps(e))
MyException('foo',)
>>> e2 = pickle.loads(pickle.dumps(e))
>>> e2.arg1
'foo'
>>> e2.arg2
'bar'

Comments

21

I like Martijn's answer, but I think a better way is to pass all arguments to the Exception base class:

class MyException(Exception):
    def __init__(self, arg1, arg2):
        super(MyException, self).__init__(arg1, arg2)        
        self.arg1 = arg1
        self.arg2 = arg2

The base Exception class' __reduce__ method will include all the args. By not making all of the extra arguments optional, you can ensure that the exception is constructed correctly.

1 Comment

And then override __str__ as needed
1

I simply do this

class MyCustomException(Exception):
    def __init__(self):
        self.value = 'Message about my error'

    def __str__(self):
        return repr(self.value)

... somewhere in code ...
raise MyCustomException

Comments

1

A completely generic solution is to add the following method to your exception class:

    def __reduce__(self):
        return (Exception.__new__, (type(self),) + self.args, self.__dict__)

This works even with keyword only arguments.

Comments

0

I was able to get a custom exception with keyword-only arguments to work with pickling like this:

class CustomException(Exception):
    def __init__(self, arg1, *, kwonly):
        super().__init__(arg1, kwonly) # makes self.args == (arg1, kwonly)
        self.arg1 = arg1
        self.kwonly = kwonly

    def __str__(self):
        # Logic here to turn the args into the real exception message
        return f"{self.arg1} ({self.kwonly})"

    # __repr__ and __eq__ are not needed but are nice for testing
    def __repr__(self):
        return f"{self.__class__.__name__}({self.arg1!r}, kwonly={self.kwonly!r})"

    def __eq__(self, other):
        return isinstance(other, CustomException) and self.args == other.args

    # This is what makes pickling work
    @classmethod
    def _new(cls, arg1, kwonly):
        return cls(arg1, kwonly=kwonly)

    def __reduce__(self):
        return (self._new, (self.arg1, self.kwonly))

Example:

import pickle

w = CustomException('arg1', kwonly='kwonly')
w2 = pickle.loads(pickle.dumps(w))
assert w == w2

By the way, this also works with custom warning classes, since those are also Exception subclasses.

Comments

0

The simplest solution is to pass all of the argument to the base-class's __init__:

class MyException(Exception):
    def __init__(self, arg1, arg2):
        super().__init__(arg1, arg2)
        self.arg1 = arg1
        self.arg2 = arg2

(Tested with Python versions 3.6, 3.8, 3.9 and 3.10.)

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.