How do you make an object holding a threading.Lock picklable for a worker process?
answer
- Instances pickle as class plus state
- Some attributes are process-local resources
- Drop what cannot travel, rebuild it after
- Two hooks, one for each direction
- __init__ never runs on unpickling
basics
~20 sDefine getstate on the class to drop the unpicklable attribute from the state it returns, and setstate to rebuild that attribute after the object is restored in the child. Only the data needs to travel; the lock is recreated locally.
solid answer
~40 sBy default an instance pickles as a reference to its class plus its `__dict__`, so it fails as soon as one attribute is a resource tied to a single process — a `threading.Lock`, a `socket.socket`, an open file object, a live connection. The hook pair is `__getstate__`, which returns whatever should be serialized (usually a copy of `__dict__` with the offending key removed), and `__setstate__`, which receives that state in the child and restores the missing attribute by constructing a fresh one. For finer control you can implement `__reduce__` or register a reducer with `copyreg`. The design question underneath is whether the resource should travel at all: a connection or a lock usually belongs to the worker process, created once there rather than reconstructed per task.
code
python · 23 linesimport pickle
import threading
class Counter:
def __init__(self):
self.value = 0
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()
counter = Counter()
counter.value = 7
clone = pickle.loads(pickle.dumps(counter))
print(clone.value, clone.lock.acquire(blocking=False))go deeper
Know that some objects simply cannot be sent to another process — locks, sockets, open files — and that the error appears when the argument is serialized, not when the worker runs. Recognising the class of problem is enough at this level.
Explain the default instance pickling path (class reference plus __dict__), then write the __getstate__ / __setstate__ pair from memory and say why __init__ does not run on the receiving side.
Demonstrate the judgment call: usually the object should not cross at all. Argue for creating the resource once per worker process and sending only plain data, and point out that a reconstructed connection per unpickle is a hidden load bug.
Own the boundary as an interface: define which objects are allowed to be task payloads, keep them plain data, and treat a class that needs pickling hooks to cross as a smell. That discipline is what lets the same task run in a thread, a process or a separate interpreter.
## How a plain instance pickles With no hooks defined, pickling an instance produces two things: - a **reference to its class**, recorded as `__module__` plus `__qualname__` the same way a function is; - and the **instance's state**, which for an ordinary object is its `__dict__`. Unpickling imports the class in the target process, allocates an instance without calling `__init__`, and then updates its `__dict__` with the restored state. Two consequences follow immediately: the class must be importable in the worker, and every *value* in `__dict__` must itself be picklable. ## Why some attributes refuse A `threading.Lock` is a handle on a synchronization primitive owned by one process's kernel or runtime. A `socket.socket` wraps a file descriptor whose number means nothing in another process. An open file object, a database connection, a generator paused mid-iteration, a running coroutine, an inner class or a lambda — all of these either cannot be reconstructed from bytes or would be actively wrong if they were. They therefore raise rather than pretend, and the traceback names the whole object graph you tried to send, not just the leaf that failed. ## The `__getstate__` / `__setstate__` contract - **`__getstate__`** is called during pickling and returns the state to store; returning a shallow copy of `__dict__` with the unpicklable keys deleted is the idiom. - **`__setstate__`** is called during unpickling with exactly that value, and its job is to put the object back into a usable condition — typically `self.__dict__.update(state)` followed by recreating whatever was dropped, such as a brand-new lock. Note that `__init__` is *not* called on the unpickling side, so anything `__init__` would have set up must be re-established in `__setstate__` or lazily on first use. If you define `__getstate__` but forget `__setstate__`, the default restore simply applies the reduced state and the attribute is silently absent until the first access blows up with `AttributeError` inside a worker — a nasty place to discover it. ## The alternatives - **`__reduce__`** gives you full control: it returns a callable and the arguments to call it with, plus optional state, so you can specify "rebuild me by calling this factory". - **`copyreg.pickle`** registers a reducer for a type you do not own, which is the escape hatch when the unpicklable attribute belongs to someone else's class. Both are heavier than the state hooks and are worth it mainly when reconstruction is more than "drop and recreate". ## The semantic trap Getting an object with a lock across the boundary **does not give you a shared lock**. The child gets a *different* lock object that guards nothing the parent can see, because the two processes have separate memories. If you actually need mutual exclusion across processes you need a cross-process primitive, not a pickled one; the state hooks are for objects that merely *carry* a lock as an implementation detail while the meaningful payload is data. The same is true for a socket or a connection: silently reconstructing one per unpickle can quietly open a connection per task, which is a load bug hiding inside a serialization fix. ## When not to ship the object at all Often the honest answer in an interview is that the object should not cross. If a worker needs a connection, let each worker process open its own once — at worker start-up, or lazily on first use keyed by `os.getpid()` — and send only the small, plain data each task needs: identifiers, ranges, configuration. That: - keeps payloads small, - avoids reconstructing expensive resources per task, - and sidesteps the whole class of pickling errors instead of patching them one class at a time. A useful rule of thumb: **if `__getstate__` is dropping more than it keeps, you are sending the wrong object.** ## Two details that bite 1. First, the class itself must be importable in the receiving process under the same `__module__` and `__qualname__` it had in the sender, because the class travels as a reference just like a function does; a class defined inside a function, or one whose module cannot be imported in a worker, fails no matter how clean its state is. 2. Second, `__slots__` changes the shape of the state: an object without a `__dict__` reduces to a two-element tuple of dictionary state and slot state, so a hand-written `__setstate__` must be prepared to receive that shape rather than a plain dictionary. Both surface only across a real boundary, which is why the round-trip test below is worth more than it looks. ## Testing it The cheapest guard is a unit test that does `pickle.loads(pickle.dumps(obj))` and asserts the rebuilt object is usable. Pickling failures otherwise surface only under a real pool, often on a rarely taken branch, and the error arrives from a worker with little context about which attribute was to blame.
- What happens if you define __getstate__ but not __setstate__?The default restore path applies the reduced state straight into `__dict__`, so the object arrives in the worker without the attribute you dropped. Nothing raises at unpickle time; the failure is a later `AttributeError` on first use, inside a worker process, far from the class that caused it. Either add `__setstate__` or make the attribute lazy so the first access recreates it.
- Does the rebuilt lock still synchronize the parent and the worker?No. The child constructs a completely separate lock object in its own memory; acquiring it blocks nothing in the parent. The state hooks are appropriate when the lock is an implementation detail of an object whose real payload is data. Genuine cross-process mutual exclusion needs a primitive designed for it, and reconstructing a socket or connection per unpickle can likewise open one resource per task by accident.
- When would you use __reduce__ instead of the state hooks?When rebuilding is not "restore this dict" but "call this factory". `__reduce__` returns a callable plus its arguments, so it can route reconstruction through a constructor, a registry lookup or a cache. `copyreg.pickle` does the same for a type you do not own. Reach for them when the class has no meaningful `__dict__` to restore, or when the object must be deduplicated on arrival rather than copied.
It is like shipping a filing cabinet without its key: you send the folders, and the receiving office cuts a new key of its own. Sending the original key would be pointless — it opens a cabinet that stayed behind.
saying these in an interview costs you the question
- Thinks copy.deepcopy can move a lock between processes
- Believes a pickled lock still synchronizes both processes
- Drops the attribute in __getstate__ and never rebuilds it
- Assumes __init__ runs again when the object is unpickled
- Says a global variable avoids the problem entirely
- Rebuilds a connection per task without noticing the cost