Programming
Python
Serialization
Pickling Error
Thread Safety

_pickle.PicklingError Could not serialize object TypeError can't pickle _thread.RLock objects

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

This error appears when Python tries to serialize an object graph that contains a thread lock. pickle can only store objects that have a meaningful portable state, and _thread.RLock does not. The fix is usually to separate runtime-only synchronization objects from the data you want to persist or send to another process.

Why RLock Cannot Be Pickled

An RLock is a re-entrant lock used for thread synchronization. Its internal state is tied to the running interpreter, the owning thread, and the current lock count. That state has no useful cross-process representation, so pickle refuses to serialize it.

You can reproduce the failure with a small class:

python
1import pickle
2import threading
3
4
5class Cache:
6    def __init__(self):
7        self.data = {"status": "ready"}
8        self.lock = threading.RLock()
9
10
11cache = Cache()
12
13try:
14    payload = pickle.dumps(cache)
15except Exception as exc:
16    print(type(exc).__name__, exc)

The important detail is not the exact exception type. The important detail is that the lock is part of the object graph being serialized.

Remove Runtime-Only Fields from Serialized State

If the lock only protects in-memory access and does not represent business data, exclude it from the pickled state and recreate it after deserialization.

python
1import pickle
2import threading
3
4
5class Cache:
6    def __init__(self):
7        self.data = {"status": "ready"}
8        self.lock = threading.RLock()
9
10    def __getstate__(self):
11        state = self.__dict__.copy()
12        state.pop("lock", None)
13        return state
14
15    def __setstate__(self, state):
16        self.__dict__.update(state)
17        self.lock = threading.RLock()
18
19
20cache = Cache()
21payload = pickle.dumps(cache)
22restored = pickle.loads(payload)
23
24print(restored.data)
25print(type(restored.lock).__name__)

This pattern is common when the serialized object contains caches, database connections, thread pools, loggers, or other process-local resources.

Prefer Serializable Data Structures at Process Boundaries

If you are passing work to multiprocessing, job queues, or distributed frameworks, design the message as plain data rather than a live object with synchronization primitives attached.

python
1def build_job_payload(user_id: int, action: str) -> dict:
2    return {
3        "user_id": user_id,
4        "action": action,
5    }
6
7
8payload = build_job_payload(42, "refresh")
9serialized = pickle.dumps(payload)
10print(serialized[:10])

A plain dictionary, list, tuple, dataclass with serializable fields, or similar structure is much easier to transport safely than a service object.

Dataclasses Need the Same Separation

This problem also appears in dataclasses. The fix is the same: keep the lock out of the serialized state.

python
1from dataclasses import dataclass, field
2import threading
3
4
5@dataclass
6class SafeCounter:
7    value: int = 0
8    lock: threading.RLock = field(default_factory=threading.RLock, repr=False)
9
10    def __getstate__(self):
11        return {"value": self.value}
12
13    def __setstate__(self, state):
14        self.value = state["value"]
15        self.lock = threading.RLock()

The lock exists for safe mutation during runtime, but it should not be treated as durable state.

Do Not Reach for a Different Pickler First

Libraries such as dill or cloudpickle can serialize more Python objects than the standard library, but they do not magically make thread synchronization state portable or semantically meaningful. Even if a tool appears to serialize a larger object graph, you still need to ask whether recreating that runtime state on the other side is correct.

In many codebases, switching serializers only hides the design issue for a while. A clean boundary between serializable data and live runtime resources is usually the stronger fix.

Common Pitfalls

The most common mistake is trying to pickle a service object instead of the data produced by that service. Service objects often contain locks, connections, executors, or clients that cannot be serialized safely.

Another problem is adding a lock to an otherwise serializable class later and forgetting that background workers or caches still pickle instances of that class.

It is also easy to misuse __getstate__ by removing the lock during pickling but forgetting to recreate it in __setstate__. That leaves the restored object in an invalid runtime state.

Finally, using a more powerful serializer does not automatically make concurrent state correct after deserialization.

Summary

  • '_thread.RLock objects are runtime synchronization primitives, not portable serialized state.'
  • 'pickle fails because a lock cannot be meaningfully reconstructed from bytes alone.'
  • Exclude locks with __getstate__ and recreate them with __setstate__.
  • Send plain data across process or network boundaries whenever possible.
  • Treat serialization errors as a design signal, not just a library limitation.

Course illustration
Course illustration

All Rights Reserved.