What does object.__getstate__() return for a plain instance versus a __slots__ class?
answer
- The inherited default, not a hand-rolled one
- Its shape depends on the class layout
- Slot values need their own compartment
- A two-element tuple pairs the two spaces
- The dict it returns is not a copy
basics
~20 sIt returns the live instance dict when only ordinary attributes are set, a two-element tuple pairing that dict (or None) with a dict of slot values when slots are populated, and None when there is nothing set at all.
solid answer
~40 sSince **3.11** `object.__getstate__` is a real inherited method, so any class can call `super().__getstate__()` to get the default state instead of hand-rolling it. It has three shapes. With ordinary attributes only, it returns the instance `__dict__` itself -- the live mapping, not a copy, so you must copy it before editing. With `__slots__` values set, it returns a two-element tuple: the instance dict or `None` first, then a dict mapping slot names to their values. With nothing set at all, it returns `None`, and a `None` state means `__setstate__` is skipped on unpickling. The tuple form matters because the default restore path understands it: given `(dict_or_None, slots_dict)` it merges the first and setattrs the second, which is how slotted classes pickle without any hooks at all.
code
python · 19 linesclass Plain:
def __init__(self):
self.a = 1
class Slotted:
__slots__ = ("b",)
def __init__(self):
self.b = 2
class Mixed(Slotted):
def __init__(self):
super().__init__()
self.c = 3
p = Plain()
print(p.__getstate__())
print(p.__getstate__() is p.__dict__)
print(Slotted().__getstate__())
print(Mixed().__getstate__())go deeper
It is enough to know that the default state is normally the instance __dict__, and that a class can ask the interpreter for that default rather than building it by hand.
Be able to name all three shapes -- dict, two-element tuple with slot values, or None -- and to explain why slots need their own compartment. Mention that the dict handed back is the live one and must be copied before editing.
Show where calling the inherited default beats hand-rolling: a base class or mixin producing correct state for subclasses whose layout it does not control. Note that a class gaining __slots__ changes the shape, so generic code must handle all three.
Weigh the portability cost of depending on the inherited method against the duplication of hand-written state logic, and set a convention so that classes across a codebase agree on how state is produced, versioned and evolved.
## The method itself Before 3.11, `object` had no `__getstate__`. The default state was computed by internal pickle machinery, `hasattr(obj, "__getstate__")` was `False` for an ordinary instance, and a subclass that wanted "the default, minus one field" had to reconstruct the default by hand: `self.__dict__.copy()`, plus explicit slot handling if the class had slots. Python 3.11 promoted it to a real inherited method, so `super().__getstate__()` is now the supported way to ask for the default and then adjust it. ## The three shapes ```python class Plain: def __init__(self): self.a = 1 class Slotted: __slots__ = ("b",) def __init__(self): self.b = 2 class Mixed(Slotted): def __init__(self): super().__init__() self.c = 3 print(Plain().__getstate__()) # {'a': 1} print(Slotted().__getstate__()) # (None, {'b': 2}) print(Mixed().__getstate__()) # ({'c': 3}, {'b': 2}) ``` * **Ordinary attributes only** -- the instance `__dict__`. * **Slot values present** -- a two-element tuple `(dict_or_None, slots_dict)`. The first element is the instance dict if the class also has one and it is non-empty, otherwise `None`; the second maps each *set* slot name to its value. An unset slot is simply absent from that mapping. * **Nothing set** -- `None`. That includes a slotted instance whose slots have all been deleted. ## Why the tuple exists Slot values do not live in `__dict__`; they occupy fixed storage on the instance. A single dict therefore cannot express the state of a class that has both kinds of attribute, because there would be no way to tell on restore which name goes where. The tuple keeps the two spaces separate, and the default restore path understands it: it merges the first element into `__dict__` and assigns the second element's entries with ordinary attribute assignment. That is why a class using slots pickles and unpickles correctly with no hooks written at all -- the default state already speaks the tuple form. ## The aliasing trap The dict that comes back is the **live** instance mapping: `Plain().__getstate__() is instance.__dict__` is `True`, and the first element of the tuple form is the live dict too. The modern-looking idiom ```python def __getstate__(self): state = super().__getstate__() # the live __dict__ for a plain class del state["lock"] # deletes the attribute from self return state ``` silently strips the attribute off the object being pickled. Copy first -- `dict(super().__getstate__())` for the plain case -- and remember that for the tuple form you have two mappings to copy, not one, and that the shape you get depends on whether the class has slots, so code written against one shape breaks when someone adds `__slots__` later. ## Practical guidance For a class you control with no slots, `self.__dict__.copy()` remains the clearest and most portable spelling of "the default state", and it works identically on 3.10 and 3.14. Calling `super().__getstate__()` earns its place in a mixin or base class that must produce a correct default for subclasses it has never seen -- including subclasses that may or may not use slots -- since reproducing the tuple logic by hand is exactly the kind of duplication that goes stale. Whichever spelling you choose, handle all three return shapes if the code is generic, copy before mutating, and remember the `None` case: returning it means the restore hook will not be called at all. ## Interview framing This is differentiator material rather than a gate. Nobody is marked down for not knowing the release that added the method. What does read well is the underlying model: state is the instance's data, slots live outside `__dict__` so they need their own compartment, and the default the interpreter computes is the same thing your handwritten hook is trying to approximate. ## Reading the tuple correctly Generic code that inspects the default state has to branch on all three shapes, and the order of the checks matters. Test for `None` first, because it means there is nothing to work with and, if returned as your own state, it suppresses the restore hook entirely. Then test for a tuple, which is the slots-bearing form; unpack it as `instance_dict, slot_values`, remembering that the first element may itself be `None` when the class has no instance dict or that dict is empty. Only the remaining case is a plain mapping. Code that assumes the mapping case and indexes the result with a string key will raise `TypeError` the moment somebody adds `__slots__` to the class, and that failure surfaces at dump time in whatever process happened to be serialising the object.
- How did code get the default state before 3.11?It rebuilt it by hand, normally as `self.__dict__.copy()`, and dealt with slot values explicitly if the class had any. `object` carried no `__getstate__` at all, so `super().__getstate__()` raised `AttributeError` and `hasattr(obj, "__getstate__")` was `False` for an ordinary instance -- a check some libraries used as a feature test.
- Why can a class with __slots__ be pickled correctly with no hooks defined?Because the default state already carries the slot values in its own compartment of the two-element tuple, and the default restore path knows that shape: it merges the first element into `__dict__` and assigns the second element's entries as attributes. No custom hook is needed unless one of those values is unpicklable or should be rebuilt.
saying these in an interview costs you the question
- Says the default state is always a dict
- Thinks slot values appear in __dict__
- Mutates the returned dict without copying it
- Claims slotted classes cannot be pickled without hooks
- Assumes super().__getstate__() works on any version
- Treats a None return as an error condition