skip to content

__getstate__ and __setstate__

By default an instance pickles its __dict__, so a lock or an open file handle travels with it and breaks. __getstate__ drops those fields and __setstate__ rebuilds them, and interviewers ask for it.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

4

How do __getstate__ and __setstate__ let you pickle an object holding a threading.Lock?

level: middleimportance: must knowfreq 55%

answer

  1. The default state carries everything
  2. Some attributes cannot cross the wire
  3. Filter going out, rebuild coming in
  4. Copy the dict before deleting from it
  5. Assign a fresh primitive in the restore hook

basics

~10 s

getstate returns a copy of the instance dict with the lock key removed, so it is never serialised. setstate merges the stored data back and creates a fresh threading.Lock on the restored object.

solid answer

~40 s

By default the state is the whole instance `__dict__`, so a `threading.Lock` attribute travels with it and `pickle.dumps` fails immediately with `TypeError: cannot pickle '_thread.lock' object`. The fix is to define `__getstate__` that takes a **copy** of `self.__dict__`, deletes the lock entry and returns the rest, and `__setstate__` that does `self.__dict__.update(state)` and then assigns a brand new `threading.Lock()`. Copying matters: `self.__dict__` -- and, on 3.11+, the dict that `object.__getstate__()` hands back -- is the *live* mapping, so deleting from it would strip the attribute off the object being pickled. The same pattern covers open files, sockets, database connections and any expensive derived cache: drop what cannot or should not cross the wire, rebuild it on arrival. Because `__init__` never runs on unpickling, `__setstate__` is the only place that rebuild can happen.

code

python · 20 lines
python
import pickle
import threading

class FlagCache:
    def __init__(self, flags):
        self.flags = flags
        self.lock = threading.Lock()

    def __getstate__(self):
        state = self.__dict__.copy()
        del state["lock"]
        return state

    def __setstate__(self, state):
        self.__dict__.update(state)
        self.lock = threading.Lock()

cache = FlagCache({"new_checkout": True})
restored = pickle.loads(pickle.dumps(cache))
print(restored.flags, restored.lock is not cache.lock)

go deeper

for a junior

Know that some attributes cannot be pickled and that a class can choose what to store. Being able to say that a synchronisation primitive or an open file has to be left out and recreated afterwards is enough at this level.

for a middle

Write the pair from memory: a copied __dict__ minus the offending key on the way out, an update plus a freshly constructed replacement on the way in. Name the dump-time TypeError, and explain why copying the dict first is not optional.

for a senior

Show judgement about what to rebuild eagerly and what to defer. Talk about not pickling objects mid-operation, about keeping __setstate__ free of I/O that can fail, and about the round-trip test that catches a missing rebuild before production does.

for a principal

Frame it as a resource-ownership boundary: which objects in a system are allowed to be serialised at all, what the stored state is permitted to contain, and how you stop a state format that mirrors class internals from becoming an accidental long-lived schema.

## Why the default breaks With no hooks defined, an instance pickles its `__dict__`, and every value in that dict must be picklable in turn. A synchronisation primitive is not: it wraps an operating-system object with no meaningful serialised form, and `pickle.dumps` refuses with `TypeError: cannot pickle '_thread.lock' object`. Note *when* that happens -- at dump time, not at load time. Files, sockets, database connections, live generators and compiled callables fail the same way, and a cache of results may pickle fine yet be pure waste in the stream. ## The idiom ```python import pickle import threading class FlagCache: def __init__(self, flags): self.flags = flags self.lock = threading.Lock() def __getstate__(self): state = self.__dict__.copy() # copy first del state["lock"] # then drop the unpicklable part return state def __setstate__(self, state): self.__dict__.update(state) # restore what was stored self.lock = threading.Lock() # rebuild what was not ``` Three things carry the answer. First, `__getstate__` returns a *filtered copy* of the state. Second, `__setstate__` merges what was stored. Third -- and this is the part candidates forget -- `__setstate__` recreates the dropped attribute, because nothing else will: the unpickler allocates a blank instance and never calls `__init__`. ## Copy, do not mutate `self.__dict__` is the object's live attribute mapping. Writing `state = self.__dict__` and then `del state["lock"]` deletes the lock from the running instance -- pickling an object quietly breaks it. The same trap applies to the modern spelling: since 3.11 you can write `state = super().__getstate__()`, but for an ordinary class that returns the live `__dict__` itself, not a copy, so it must be copied before you edit it. On 3.10 and earlier the `super()` call does not exist at all and `self.__dict__.copy()` is the only spelling. ## Rebuild versus reconnect A fresh lock is the easy case: locks carry no identity worth preserving, and the restored object simply gets an unheld one. Other resources demand a decision rather than a reflex: * **Connections and file handles.** Reopening eagerly inside `__setstate__` makes unpickling do I/O, which can fail in a context where nothing is prepared to handle it. Storing enough to reconnect -- a path, a DSN, an offset -- and opening lazily on first use is usually the better shape. * **Derived caches.** Rebuilding is cheap and correct; storing them bloats the stream and risks staleness, since the cached values were computed against inputs that may have moved on. * **Held state.** If the lock was held or a transaction was open at dump time, there is nothing honest to restore. Do not pickle mid-operation objects; pickle them at rest. ## Ownership after the round trip A restored object is a genuinely separate object with genuinely separate primitives. Two processes that unpickle the same bytes each get their own lock, and those locks coordinate nothing with each other -- they only ever protect the object each one holds. Candidates who imagine a lock "reconnects" to some shared original have the model wrong, and interviewers probe exactly there. ## Testing it The round trip belongs in a unit test, not in production discovery. `pickle.loads(pickle.dumps(obj))` and then assert on the attributes that matter: that the rebuilt attribute exists, that it is a distinct object from the original's, and that the restored instance passes whatever invariant check `__init__` would have guaranteed. That test is what catches the very common half-fix in which someone writes `__getstate__` to make the dump succeed and forgets `__setstate__` entirely -- a change that turns a loud `TypeError` at dump time into a quiet `AttributeError` much later, at first use of the missing attribute. ## When there is nothing to drop Do not add these hooks by reflex. If every attribute is plain data, the default state is correct, faster to reason about, and one less thing to keep in step with the constructor. The hooks earn their place precisely when the object owns something the byte stream cannot carry.

  • Why copy self.__dict__ instead of deleting the key from it directly?
    Because `self.__dict__` is the object's live attribute mapping, not a snapshot. Deleting from it removes the attribute from the instance being pickled, so serialising an object would break it in place. `self.__dict__.copy()` -- or a copy of whatever `super().__getstate__()` returns on 3.11+ -- keeps the mutation local to the state you are about to hand over.
  • The lock protected a counter. Is the restored object immediately safe to share across threads?
    It has a fresh, unheld lock, so it is usable -- but only if `__setstate__` re-established every invariant the constructor would have. Nothing about the original lock's ownership carries over, and two processes unpickling the same bytes get independent locks that coordinate nothing between them. Finish restoring before publishing the object to other threads.
  • Would you reopen a database connection inside __setstate__ or leave it lazy?
    Usually lazy. Reopening eagerly makes unpickling perform I/O that can fail in a context with no error handling around it, and the restored object may never be used. Store the parameters needed to reconnect -- a path or a DSN -- and open on first use, keeping `__setstate__` cheap and total.

saying these in an interview costs you the question

  • Deletes the key from self.__dict__ rather than a copy
  • Says locks pickle fine and reconnect on load
  • Writes __getstate__ but no __setstate__ rebuild
  • Sets the dropped attribute to None permanently
  • Thinks the failure appears at load time, not dump time
  • Expects two restored copies to share one lock

context

open as a page

What do __getstate__ and __setstate__ do when Python pickles and unpickles an object?

level: juniorimportance: should knowfreq 40%

basics

~10 s

getstate returns the data written into the pickle, which by default is the instance dict. setstate receives that data on unpickling and rebuilds the object from it. Unpickling never calls init.

open as a page

Why would __setstate__ never run when a cached feature-flag object is unpickled?

level: seniorimportance: should knowfreq 25%

basics

~20 s

Because the stored state was None. CPython 3.14 applies a state-restoring step only when there is state to apply, so a getstate returning None means setstate is skipped entirely and the rebuild silently never happens.

open as a page

What does object.__getstate__() return for a plain instance versus a __slots__ class?

level: middleimportance: nice to knowfreq 15%

basics

~20 s

It 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.

open as a page