Why does `copy.deepcopy()` raise TypeError on an object holding a `threading.Lock`, and how do you share it instead?
answer
- Deep copy walks every attribute
- The error names a protocol nobody invoked
- Shallow copy of the same object succeeds
- A lock is an identity, not a value
- Rebind the handle inside the hook
basics
~10 sdeepcopy recurses into every attribute, and a threading.Lock cannot be reduced, so the pickle fallback raises TypeError: cannot pickle '_thread.lock' object. Define deepcopy to deep-copy the data fields and rebind the lock by reference.
solid answer
~40 s`copy.deepcopy()` walks the whole object graph, and for an attribute with no `__deepcopy__` it falls through to the pickle protocol. A `threading.Lock` has no reduce, so the pass dies with `TypeError: cannot pickle '_thread.lock' object` even though nobody asked to pickle anything. The fix is a `__deepcopy__(self, memo)` that allocates with `cls.__new__(cls)`, registers `memo[id(self)] = new`, deep-copies the data attributes, and assigns the lock across by plain reference so both objects use one lock. The judgement call is which handles to share and which to recreate: share the lock when the copies still guard one piece of state, create a fresh `threading.Lock()` when the copy owns state of its own. Duplicating the lock's *state* is never right — two mutexes over one accumulator exclude nobody.
code
python · 31 linesimport copy
import threading
class Stage:
def __init__(self, name):
self.name = name
self.totals = {"hits": 83}
self.lock = threading.Lock()
try:
copy.deepcopy(Stage("parse"))
except TypeError as exc:
print("deepcopy failed:", exc)
class SharedLockStage(Stage):
def __deepcopy__(self, memo):
cls = type(self)
new = cls.__new__(cls)
memo[id(self)] = new
new.name = self.name
new.totals = copy.deepcopy(self.totals, memo)
new.lock = self.lock
return new
original = SharedLockStage("parse")
clone = copy.deepcopy(original)
print(clone.lock is original.lock, clone.totals is original.totals)go deeper
Remember that copy.deepcopy() can fail outright, and that a TypeError mentioning pickling usually means the object holds something like a lock or a socket rather than plain data.
Explain the fallback path that produces the error and write the hook correctly: cls.__new__(cls), register in memo, deep-copy the data attributes with the memo, rebind the handle by reference.
Demonstrate the judgement, not just the fix: decide per attribute whether the copies should share, recreate or lazily rebuild the resource, and explain why a duplicated lock over shared state is a silent correctness bug rather than a performance one.
Own the design question behind it — whether cloning stateful objects is the right way to fan out work at all, versus a factory that takes shared collaborators as explicit parameters and removes the copy semantics from the codebase entirely.
## The failure, and why pickling is involved at all A log-ingest pipeline builds one configured stage object and clones it per worker with `copy.deepcopy()`. The stage carries plain data — a name, a dict of counters sitting at an 83% cache-hit rate, a running mean of parse latency — and one `threading.Lock` guarding the counters. The clone raises: ``` TypeError: cannot pickle '_thread.lock' object ``` Nothing was pickled. The message comes from `deepcopy`'s own fallback path: for each object with no `__deepcopy__` and no entry in its internal dispatch table, it asks `copyreg.dispatch_table`, then calls `__reduce_ex__(4)` and rebuilds from the result. That is the pickle protocol used purely in memory. A lock is a thin wrapper over an OS primitive with no meaningful reduce, so it refuses, and the refusal surfaces as a `TypeError` from a copy the caller never connected to serialization. Recognizing this as "deep copy reached something unreducible" rather than "someone pickled my object" is most of the diagnosis. Note what does *not* fail: `copy.copy()` on the same stage succeeds, because a shallow copy rebinds the existing attribute values into a new instance without recursing into any of them. The lock is not copied; it is referenced. That contrast is a useful probe when you meet this error in the wild. ## Why duplicating the lock would be worse than the error It is tempting to read the `TypeError` as an annoyance to route around — swap in a fresh `threading.Lock()` for the clone and move on. Sometimes that is exactly right, and sometimes it silently destroys the invariant the lock existed to protect. In the pipeline above, all worker clones update one shared rolling mean. If every clone gets its own lock, each one acquires a mutex nobody else respects, the read-modify-write on the shared accumulator interleaves freely, updates are lost, and the reported mean drifts away from the true value by a small, irreproducible floating-point margin that grows with load. That is the worst class of bug this leaf produces: no exception, no crash, just numbers that are slightly wrong and a discrepancy nobody can reproduce on a single worker. The rule to state in an interview: **a lock is an identity, not a value.** Copying its bits means nothing; what matters is whether the copies are meant to exclude each other. Answer that question first, then write the hook. ## Writing the hook ```python def __deepcopy__(self, memo): cls = type(self) new = cls.__new__(cls) memo[id(self)] = new new.name = self.name new.totals = copy.deepcopy(self.totals, memo) new.lock = self.lock # shared on purpose return new ``` Four things are load-bearing. `cls = type(self)` keeps subclass copies correct. `cls.__new__(cls)` skips `__init__`, which would allocate a *new* lock and defeat the point. `memo[id(self)] = new` comes before any recursion so cycles terminate. And `copy.deepcopy(self.totals, memo)` passes the memo down so shared subgraphs stay shared. The same shape covers the other handles that trigger this error: an open file object, a socket, a database connection, a compiled callback registry, a thread or a running task. For each, choose one of three treatments — share the reference, create a fresh one, or set the attribute to `None` and reconnect lazily on first use. Sharing is right when the copies collaborate on one resource; a fresh instance is right when the copy is genuinely independent; `None` plus lazy rebuild is right when the resource is expensive and the copy may never need it. ## Keeping the hook honest The cost of a hand-written `__deepcopy__` is that it is a second constructor. Any attribute added to `__init__` later and not added to the hook simply vanishes from copies, usually as an `AttributeError` far from the change. Two habits contain that. Copy the bulk generically and then override only the exceptions — start from `new.__dict__.update(copy.deepcopy(...))` over a filtered dict, or explicitly list the shared names in a class-level tuple the hook reads. And test the hook: assert that the copy's data attributes are not the originals and that the shared handle *is* the original, so a future edit that duplicates the lock fails a test instead of drifting a metric. Finally, ask whether the clone is needed at all. A stage built by a factory function taking the shared lock as a parameter has no copy semantics to get wrong, and in a pipeline that constructs workers once at startup, that is usually the smaller design.
- When should each copy get a brand-new `threading.Lock()` rather than sharing the original?When the copies own separate state. A lock protects an invariant over specific data; if the copy's data is its own, sharing one lock only serializes unrelated work and creates a contention point. Share the lock when the copies still mutate one shared structure, allocate a fresh one when the copy is independent, and never copy the lock's internal state — that is meaningless either way.
- Why does `copy.copy()` succeed on the same object where `copy.deepcopy()` fails?A shallow copy produces a new instance whose attributes are rebound to the very same values; it never recurses into them, so the lock is referenced rather than copied and nothing consults the reduce protocol for it. Deep copy walks the whole graph, reaches the lock, finds no `__deepcopy__` and no reductor, and raises. The contrast is a fast way to confirm the failing attribute is a handle rather than the object itself.
- How would you handle an attribute that must be neither shared nor duplicated, such as an open connection?Set it in the hook rather than copying it: assign `None` and have the property or accessor reconnect lazily on first use. The copy then starts with no connection, acquires its own when it needs one, and never inherits a socket that a second owner might close underneath it. Document the attribute as rebuilt-on-demand so the next reader does not add it back to the copy list.
saying these in an interview costs you the question
- Calls deep copy safe because it duplicates everything
- Gives each copy its own lock without asking why
- Says someone must be pickling the object
- Tries to copy the lock's internal state by hand
- Writes the hook but omits `memo[id(self)] = new`
- Switches to `copy.copy()` and calls nested data protected