Customising pickle with copyreg
The copyreg module gives you precise control over customising pickle behaviour for your classes. Rather than relying on pickle’s default object introspection, you register reducer functions that tell pickle exactly how to serialise and reconstruct each type. By registering pickling functions with copyreg, pickle knows exactly how to handle your custom classes, making them portable across Python versions and implementations.
Pickle usually works by walking an object’s internal dictionary and saving whatever it finds. That approach breaks down when your object holds things pickle cannot reach: file handles, network sockets, lambdas defined inside a function body, or state that only makes sense at runtime. The copyreg module handles all of these cases by letting you specify a reconstruction recipe that pickle follows instead of guessing.
The case for copyreg
By default, pickle can serialize most Python objects by storing their __dict__. However, this approach has limitations:
- It may not work correctly for objects with complex internal state
- It can be inefficient for objects that contain references to functions or other unpicklable objects
- It doesn’t preserve class methods or custom attributes set outside
__init__
The copyreg module lets you define exactly how your objects should be reduced to their fundamental components and rebuilt.
A reducer is simply a callable that accepts an object and returns a reconstruction recipe: a tuple containing a constructor function and the arguments needed to invoke it. When pickle encounters a registered type, it calls your reducer instead of its default introspection logic, giving you full control over what gets serialized and how the object is recreated on the other side.
Registering a reducer function
The core of copyreg is the register function, which takes a class and a reducer:
import copyreg
import pickle
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point({self.x}, {self.y})"
# Register a reducer function
def reduce_point(point):
# Return constructor and arguments needed to recreate
return (Point, (point.x, point.y))
copyreg.pickle(Point, reduce_point)
# Now Point objects can be pickled
p = Point(3, 4)
data = pickle.dumps(p)
unpickled = pickle.loads(data)
print(unpickled) # Point(3, 4)
The reducer function must return a tuple of (constructor, args) where constructor is a callable that can recreate the object from args.
The registration step binds your reducer to the class permanently within the current process. Once registered, every call to pickle.dumps on an instance of that class goes through your reducer, not pickle’s default mechanism. This indirection is what makes copyreg so flexible: you can change how an object is serialized without touching its source code.
External reducers versus reduce
While you can define __reduce__ directly on a class, using copyreg offers several advantages:
- Separation of concerns: The serialization logic lives outside the class
- Version compatibility: You can change the reducer without modifying the class
- Inheritance: Subclasses automatically inherit the reducer unless overridden
Using copyreg instead of __reduce__ also lets you register reducers for classes you do not own. If you are working with a third-party library whose objects need custom pickling, defining __reduce__ on their class is intrusive and risks breaking on library updates. With copyreg, you write the reducer in your own code and register it at import time. The library class stays untouched, and pickle routes through your reducer transparently.
Similarly, if two independent modules both need to pickle the same class differently for different use cases, copyreg lets each module register its own reducer. The last registration wins, so import order matters. In practice, this means you should register reducers early, typically at module level during import, to avoid surprises from downstream code overwriting your registration.
Handling complex objects
For objects with state that isn’t easily captured in __init__ arguments, you can use __getstate__ and __setstate__ alongside copyreg:
import copyreg
import pickle
class DataProcessor:
def __init__(self, name):
self.name = name
self.cache = {} # Not picklable by default
def __getstate__(self):
# Return state to pickle (exclude unpicklable items)
return {"name": self.name}
def __setstate__(self, state):
# Restore state
self.name = state["name"]
self.cache = {} # Recreate unpicklable attributes
def __repr__(self):
return f"DataProcessor({self.name}, cache_size={len(self.cache)})"
def reduce_processor(processor):
return (
DataProcessor,
(processor.name,),
processor.__getstate__()
)
copyreg.pickle(DataProcessor, reduce_processor)
# Test it
processor = DataProcessor("my_processor")
processor.cache["key"] = "value"
data = pickle.dumps(processor)
unpickled = pickle.loads(data)
print(unpickled) # DataProcessor(my_processor, cache_size=0)
The key insight here is that copyreg doesn’t just serialize object state; it lets you explicitly control how objects are reconstructed, which is essential when some attributes cannot be serialized directly. The reducer function decides the constructor arguments, while __getstate__ and __setstate__ handle any extra state that doesn’t fit into the constructor. Together they give you complete control over the pickling lifecycle without modifying the class itself. This approach is particularly useful for objects that hold file handles, network connections, or other non-serializable resources.
Pickling functions and classes
By default, pickle can reference functions by name if they are available in the module’s global scope. However, for lambda functions or dynamically created classes, you need to register reducers:
import copyreg
import pickle
# This won't work with lambdas by default:
# point = Point(1, 2) # where Point is defined inline
# Instead, define at module level and register:
class Vector:
def __init__(self, x, y, z):
self.coords = (x, y, z)
def __repr__(self):
return f"Vector{self.coords}"
def reduce_vector(v):
return (Vector, v.coords)
copyreg.pickle(Vector, reduce_vector)
v = Vector(1, 2, 3)
print(pickle.loads(pickle.dumps(v))) # Vector(1, 2, 3)
The reducer for Vector is straightforward because the constructor already accepts all necessary state as separate arguments. When designing your own reducers, aim for this pattern: make the constructor serve as the natural reconstruction point. If your objects require multi-step setup or have circular references, you may need a more elaborate reducer that handles those edge cases explicitly. The Vector example also demonstrates why module-level class definitions are important: dynamically created classes lack a stable import path and will fail during unpickling unless you register them with copyreg.
Versioning with constructors
You can use reducers to handle version migration by creating constructors that accept both old and new argument formats:
import copyreg
import pickle
class Config:
def __init__(self, setting, value=None):
# Support both old format (setting only) and new (setting, value)
if value is None:
# Old format: setting was a dict
if isinstance(setting, dict):
self.settings = setting
else:
self.settings = {"value": setting}
else:
# New format: explicit setting and value
self.settings = {setting: value}
def __repr__(self):
return f"Config({self.settings})"
def reduce_config(config):
# Always save in new format
settings = config.settings
if len(settings) == 1:
key, value = next(iter(settings.items()))
return (Config, (key, value))
else:
return (Config, (settings,))
copyreg.pickle(Config, reduce_config)
# New-style object
c1 = Config("debug", True)
data1 = pickle.dumps(c1)
print(pickle.loads(data1)) # Config({'debug': True})
# Old-style object (if you had one)
c2 = Config({"theme": "dark", "timeout": 30})
data2 = pickle.dumps(c2)
print(pickle.loads(data2)) # Config({'theme': 'dark', 'timeout': 30})
This approach lets you evolve your class while maintaining backward compatibility with previously pickled data.
The Config example shows how reducers can smooth over API changes without breaking existing pickled objects. By always emitting the new argument format, you ensure that future unpickling always uses the current constructor signature. If you plan to ship pickled data between services or save it to disk for long-term storage, planning your reducer around forward compatibility from the start will save you painful migration headaches later on.
A practical strategy is to version your reducer output. Include a version number in the reconstruction tuple alongside the constructor arguments. When unpickling, the constructor reads the version and applies any needed migrations. This decouples the data format from the class API and lets you evolve both independently over time. If you skip versioning and later need to change the constructor signature, you will have to decide between breaking backward compatibility or maintaining a constructor that grows increasingly complex as it supports more legacy formats.
Practical example: pickling file objects
One common pain point is pickling objects that contain file handles. Here’s how to handle it:
import copyreg
import pickle
from io import StringIO
class LogBuffer:
"""A buffer that can be pickled by excluding the file handle."""
def __init__(self, filename):
self.filename = filename
self.buffer = StringIO()
self._file = None
def write(self, message):
self.buffer.write(message)
def flush(self):
# Write to actual file if available
if self._file:
self._file.write(self.buffer.getvalue())
self._file.flush()
self.buffer.truncate(0)
def __getstate__(self):
state = self.__dict__.copy()
# Don't pickle the file handle
state["_file"] = None
return state
def __setstate__(self, state):
self.__dict__.update(state)
# Reopen file if it existed
if self.filename:
self._file = open(self.filename, "a")
def __repr__(self):
return f"LogBuffer({self.filename}, buffered={len(self.buffer.getvalue())})"
def reduce_log_buffer(buffer):
return (LogBuffer, (buffer.filename,))
copyreg.pickle(LogBuffer, reduce_log_buffer)
# Example usage
log = LogBuffer("/tmp/app.log")
log.write("Line 1\n")
log.write("Line 2\n")
# Pickle it (file handle is excluded)
data = pickle.dumps(log)
print(f"Pickled size: {len(data)} bytes")
# Unpickle (file handle is re-opened)
restored = pickle.loads(data)
print(restored)
Summary
The copyreg module decouples serialization logic from class definitions, which keeps your objects clean and your pickling strategy flexible. Register a reducer for each custom type that pickle needs to handle, and let the reducer decide the constructor and arguments. Use __getstate__ and __setstate__ to manage attributes that cannot travel through the constructor directly, such as file handles or cached data. If your data format evolves over time, write version-aware constructors that accept both old and new argument shapes so that previously pickled objects remain readable. The separation that copyreg provides means you can update your serialization strategy without touching class internals, and subclasses automatically inherit the reducer unless overridden.
See Also
- pickle-module : The pickle module for object serialization
- copy-module : For shallow and deep copy operations
- python-dataclasses : Dataclasses and their serialization defaults