Python’s copyreg module lets you register custom reduce functions that control how pickle serializes objects and how copy.copy/copy.deepcopy clone them. import copyreg. Register: copyreg.pickle(type, reduce_fn) — reduce_fn(obj) must return a tuple (callable, args) or the extended (callable, args, state, listiter, dictiter) compatible with __reduce_ex__; callable(*args) reconstructs the object. Constructor: copyreg.constructor(fn) — declares fn as a valid reconstruction callable; required before pickle will accept it. Dispatch table: copyreg.dispatch_table — the global dict {type: reduce_fn}; a Pickler instance can also have its own dispatch_table. copyreg._reconstructor(cls, base, state) — the default reconstruction function used internally. Extension codes: copyreg.add_extension(module, name, code) — map a (module, name) global to a short integer code (reduces pickle size for frequent objects); copyreg.remove_extension(module, name, code); copyreg.clear_extension_cache(). Interaction with copy: copyreg registrations affect copy.copy and copy.deepcopy as well as pickle. Claude Code generates version-tolerant serialization schemas, backward-compatible pickle protocols, C-extension type pickle support, and custom deep-copy controls.
CLAUDE.md for copyreg
## copyreg Stack
- Stdlib: import copyreg, pickle, copy
- Register: copyreg.pickle(MyType, reduce_fn)
- # reduce_fn(obj) -> (callable, args) or (callable, args, state)
- Constructor: copyreg.constructor(reconstruct_fn)
- Table: copyreg.dispatch_table[MyType] = reduce_fn # same effect
- Note: affects both pickle AND copy.copy / copy.deepcopy
- for classes, prefer __reduce__ / __reduce_ex__ / __getstate__
copyreg Pickle State Pipeline
# app/copyregutil.py — reduce fns, versioned schema, C-type, extension codes
from __future__ import annotations
import copyreg
import copy
import io
import pickle
import struct
from dataclasses import dataclass, field, asdict
from typing import Any
# ─────────────────────────────────────────────────────────────────────────────
# 1. Basic reduce registration
# ─────────────────────────────────────────────────────────────────────────────
class Color:
"""
A simple RGB color type with no __reduce__ — we register via copyreg.
"""
__slots__ = ("r", "g", "b")
def __init__(self, r: int, g: int, b: int) -> None:
self.r = r
self.g = g
self.b = b
def __repr__(self) -> str:
return f"Color(r={self.r}, g={self.g}, b={self.b})"
def __eq__(self, other: object) -> bool:
if not isinstance(other, Color):
return NotImplemented
return (self.r, self.g, self.b) == (other.r, other.g, other.b)
def _reduce_color(c: Color) -> tuple:
return (_make_color, (c.r, c.g, c.b))
def _make_color(r: int, g: int, b: int) -> Color:
return Color(r, g, b)
copyreg.constructor(_make_color)
copyreg.pickle(Color, _reduce_color)
# ─────────────────────────────────────────────────────────────────────────────
# 2. Versioned schema with state migration
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class UserRecord:
"""
A record that may be pickled from older schema versions.
Uses copyreg to inject schema_version and migrate on load.
"""
username: str
email: str
role: str = "user" # added in schema v2
active: bool = True # added in schema v3
_schema_version: int = field(default=3, repr=False, compare=False)
def _reduce_user_record(u: UserRecord) -> tuple:
state = {
"username": u.username,
"email": u.email,
"role": u.role,
"active": u.active,
"_schema_version": u._schema_version,
}
return (_reconstruct_user_record, (), state)
def _reconstruct_user_record() -> UserRecord:
# placeholder constructor; real data applied via __setstate__-like mechanism
return object.__new__(UserRecord)
def _reconstruct_user_record_full(state: dict) -> UserRecord:
"""Reconstruct with schema migration."""
v = state.get("_schema_version", 1)
if v < 2:
state.setdefault("role", "user")
if v < 3:
state.setdefault("active", True)
state["_schema_version"] = 3
obj = object.__new__(UserRecord)
obj.__dict__.update(state)
return obj
def _reduce_user_record_v2(u: UserRecord) -> tuple:
"""Reduce function that packs state for migration-aware reconstruction."""
state = {
"username": u.username,
"email": u.email,
"role": u.role,
"active": u.active,
"_schema_version": 3,
}
return (_reconstruct_user_record_full, (state,))
copyreg.constructor(_reconstruct_user_record_full)
copyreg.pickle(UserRecord, _reduce_user_record_v2)
# ─────────────────────────────────────────────────────────────────────────────
# 3. Registering via dispatch_table for isolated Pickler
# ─────────────────────────────────────────────────────────────────────────────
def make_custom_pickler(
stream: io.BytesIO,
extra_reducers: dict[type, "callable"] | None = None,
) -> pickle.Pickler:
"""
Create a Pickler with a custom dispatch_table that extends copyreg defaults.
Useful when you need per-Pickler overrides without global registration.
Example:
buf = io.BytesIO()
p = make_custom_pickler(buf, {Color: lambda c: (_make_color, (c.r, c.g, c.b))})
p.dump(my_obj)
data = buf.getvalue()
"""
p = pickle.Pickler(stream)
# Start from global dispatch_table, then extend
p.dispatch_table = copyreg.dispatch_table.copy()
if extra_reducers:
p.dispatch_table.update(extra_reducers)
return p
def pickle_with_custom_table(
obj: object,
extra_reducers: dict[type, "callable"] | None = None,
) -> bytes:
"""
Pickle obj using a Pickler with an extended dispatch_table.
Example:
data = pickle_with_custom_table(Color(255, 0, 0))
"""
buf = io.BytesIO()
p = make_custom_pickler(buf, extra_reducers)
p.dump(obj)
return buf.getvalue()
# ─────────────────────────────────────────────────────────────────────────────
# 4. Extension codes (size-optimised pickle)
# ─────────────────────────────────────────────────────────────────────────────
_EXT_MODULE = "app.copyregutil"
_COLOR_EXT_CODE = 1001 # arbitrary unique integer in range [1, 2**31-1]
def register_color_extension() -> None:
"""
Register Color's constructor as an extension code to shrink pickle size.
Extension codes replace the full module+qualname string with a short int.
Example:
register_color_extension()
data = pickle.dumps(Color(0, 128, 255))
print(len(data)) # smaller than without extension
"""
try:
copyreg.add_extension(_EXT_MODULE, "_make_color", _COLOR_EXT_CODE)
except ValueError:
pass # already registered
def unregister_color_extension() -> None:
try:
copyreg.remove_extension(_EXT_MODULE, "_make_color", _COLOR_EXT_CODE)
except ValueError:
pass
# ─────────────────────────────────────────────────────────────────────────────
# 5. Utility: roundtrip + size comparison
# ─────────────────────────────────────────────────────────────────────────────
def roundtrip(obj: object) -> object:
"""Pickle and unpickle obj, returning the reconstructed value."""
return pickle.loads(pickle.dumps(obj))
def copy_roundtrip(obj: object) -> object:
"""deep-copy obj (uses copyreg dispatch table)."""
return copy.deepcopy(obj)
def pickle_size(obj: object) -> int:
"""Return the number of bytes in pickle.dumps(obj)."""
return len(pickle.dumps(obj))
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== copyreg demo ===")
# ── Color pickle roundtrip ────────────────────────────────────────────────
print("\n--- Color (copyreg.pickle) ---")
c = Color(255, 128, 0)
c2 = roundtrip(c)
print(f" original: {c}")
print(f" roundtrip: {c2}")
print(f" equal: {c == c2}")
print(f" pickle size: {pickle_size(c)} bytes")
# ── Color deepcopy ────────────────────────────────────────────────────────
print("\n--- copy.deepcopy via copyreg ---")
c3 = copy_roundtrip(c)
print(f" deepcopy: {c3} same object: {c3 is c}")
# ── UserRecord schema migration ──────────────────────────────────────────
print("\n--- UserRecord versioned schema ---")
u = UserRecord(username="alice", email="[email protected]")
u2 = roundtrip(u)
print(f" original: {u}")
print(f" roundtrip: {u2}")
print(f" equal: {u == u2}")
# Simulate loading an old v1 record (missing role, active)
v1_state = {"username": "bob", "email": "[email protected]", "_schema_version": 1}
v1_obj = _reconstruct_user_record_full(v1_state)
print(f" v1 migrated: {v1_obj}")
# ── dispatch_table per-Pickler ────────────────────────────────────────────
print("\n--- custom dispatch_table pickler ---")
data = pickle_with_custom_table(Color(0, 64, 255))
c4 = pickle.loads(data)
print(f" custom pickler roundtrip: {c4}")
# ── extension code ────────────────────────────────────────────────────────
print("\n--- extension code ---")
before = pickle_size(Color(0, 0, 0))
register_color_extension()
after = pickle_size(Color(0, 0, 0))
print(f" size without ext code: {before} bytes")
print(f" size with ext code: {after} bytes")
unregister_color_extension()
# ── copyreg.dispatch_table contents ──────────────────────────────────────
print("\n--- dispatch_table entries ---")
for t in list(copyreg.dispatch_table):
print(f" {t.__module__}.{t.__qualname__}")
print("\n=== done ===")
For the __reduce__ / __reduce_ex__ / __getstate__ / __setstate__ alternative — defining these dunder methods directly on a class is the preferred way to control pickling for classes you own; copyreg is for types you do not own (C extensions, third-party classes) or for globally overriding an existing __reduce__ without modifying the source — use dunder methods for your own classes, copyreg.pickle for foreign types. For the pickle.Pickler.dispatch_table alternative — setting dispatch_table on a Pickler instance (inheriting from copyreg.dispatch_table and overriding specific entries) is the thread-safe, non-global way to customize serialization for a single pickling operation — use Pickler.dispatch_table when you need per-session or per-thread reduce overrides without mutating the global copyreg.dispatch_table that affects all pickling. The Claude Skills 360 bundle includes copyreg skill sets covering _reduce_color/_make_color with copyreg.pickle() basic registration, UserRecord with _reduce_user_record_v2/_reconstruct_user_record_full versioned schema migration, make_custom_pickler()/pickle_with_custom_table() per-Pickler dispatch tables, register_color_extension()/unregister_color_extension() size-optimising extension codes, and roundtrip()/copy_roundtrip()/pickle_size() utilities. Start with the free tier to try pickle registration patterns and copyreg pipeline code generation.