The problem comes from pickle
using the __setattr__
method of the instance when setting the state of the slots.
The default __setstate__
is defined in load_build
in _pickle.c
line 6220.
For the items in the state dict, the instance __dict__
is updated directly:
if (PyObject_SetItem(dict, d_key, d_value) < 0)
whereas for the items in the slotstate dict, the instance's __setattr__
is used:
if (PyObject_SetAttr(inst, d_key, d_value) < 0)
Now because the instance is frozen, __setattr__
raises FrozenInstanceError
when loading.
To circumvent this, you can define your own __setstate__
method which will use object.__setattr__
, and not the instance's __setattr__
.
The docs give some sort of warning for this:
There is a tiny performance penalty when using frozen=True: __init__()
cannot use simple assignment to initialize fields, and must use object.__setattr__()
.
It may also be good to define __getstate__
as the instance __dict__
is always None
in your case. If you don't, the state
argument of __setstate__
will be a tuple (None, {'a': 5})
, the first value being the value of the instance's __dict__
and the second the slotstate dict.
import pickle
from dataclasses import dataclass
@dataclass(frozen=True)
class A:
__slots__ = ('a',)
a: int
def __getstate__(self):
return dict(
(slot, getattr(self, slot))
for slot in self.__slots__
if hasattr(self, slot)
)
def __setstate__(self, state):
for slot, value in state.items():
object.__setattr__(self, slot, value) # <- use object.__setattr__
b = pickle.dumps(A(5))
pickle.loads(b)
I personally would not call it a bug as the pickling process is designed to be flexible, but there is room for a feature enhancement. A revision of the pickling protocol could fix this in future. Unless I am missing something and aside of the tiny performance penalty, using PyObject_GenericSetattr
for all the slots might be a reasonable fix?