Python

Master Python __reduce__ for Custom Serialization

Learn how Python's __reduce__ method gives you total control over pickle serialization for objects with unpicklable resources, cached state, or third-party dependencies, plus when to prefer simpler tools.

August 2026 5 min read 15 views 0 hearts

Oh, you think you know Python serialization? You've used pickle.dump() and json.dumps(), maybe even dabbled with __getstate__ and __setstate__. But there's a hidden gem in Python's serialization toolkit that most developers never touch: __reduce__. And once you understand it, you'll see objects—and their serialization—in a completely new light.

What exactly is __reduce__?

At its heart, __reduce__ is Python's way of giving you total control over how an object gets pickled. When pickle encounters an object, it first checks if the object has a __reduce_ex__ method (for protocol versioning), and then falls back to __reduce__. If neither exists, pickle does its default thing—which usually works, until it doesn't.

The method returns a tuple that tells pickle exactly how to reconstruct your object. The simplest form returns (callable, args) where callable is a function or class that can recreate the object when called with args.

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __reduce__(self):
        return (self.__class__, (self.x, self.y))

p = Point(3, 4)
import pickle
data = pickle.dumps(p)
reconstructed = pickle.loads(data)  # Works perfectly

Why you'd actually use it

The default pickle behavior works fine for simple objects. But real-world Python gets messy. Here's where __reduce__ shines:

Database connections, file handles, or any unpicklable resource. You can't pickle an open socket or a database cursor. But with __reduce__, you can serialize the instructions to recreate that resource.

class DatabaseConnection:
    def __init__(self, host, port, db_name):
        self.host = host
        self.port = port
        self.db_name = db_name
        self.connection = None  # Can't pickle this!

    def __reduce__(self):
        # Return instructions to recreate, not the connection itself
        return (self.__class__, (self.host, self.port, self.db_name))

    def connect(self):
        # Your actual connection logic here
        pass

Objects with computed or cached state. Sometimes you want to serialize only the essential data, not every cached attribute.

class DataAnalyzer:
    def __init__(self, raw_data):
        self.raw_data = raw_data
        self._cache = {}  # Expensive computations stored here

    def __reduce__(self):
        # Only pickle the raw data, not the cache
        return (self.__class__, (self.raw_data,))

Third-party objects you can't modify. This is where __reduce__ becomes a lifesaver. If you're working with objects from a library you can't change, but they can't be pickled, __reduce__ lets you create a serialization workaround using copyreg.

The advanced form: state and iterators

The full __reduce__ tuple can include up to 5 elements: (callable, args, state, list_items, dict_items). That third position, state, gets passed to __setstate__ if the object has one. And list_items/dict_items let you append items to the reconstructed object.

class GrowingList:
    def __init__(self, initial=None):
        self.items = initial or []

    def __reduce__(self):
        return (self.__class__, ([]), None, self.items)

gl = GrowingList([1, 2, 3])
# When unpickled, pickle will create the object, then extend it with self.items

What PythonSkillset learned the hard way

At PythonSkillset, we once had a microservice that processed customer analytics. The team decided to cache serialized objects in Redis using pickle—a reasonable choice for speed. But one object kept failing to deserialize after deployments. Turns out, it contained references to module-level constants that changed between versions.

The fix? A carefully crafted __reduce__ method that stored only the constant's value, not its module reference:

# Before: Broke after deployments
class ConfigWrapper:
    # Pickle tried to save the reference to DEFAULT_CONFIG

# After: Works across versions
class ConfigWrapper:
    def __reduce__(self):
        return (self.__class__, (self._get_literal_config_copy(),))

When NOT to use it

Let's be honest: __reduce__ is not for everyday use. For 95% of your serialization needs, __getstate__ and __setstate__ give you cleaner control. Use __reduce__ when:

  • You need to change the constructor call, not just hide attributes
  • You're working with objects that require special import logic to reconstruct
  • You want to customize serialization of objects you can't modify (using copyreg.pickle())

Otherwise, stick with the simpler tools. __reduce__ is powerful, but power comes with complexity. Mistake in the return tuple, and you'll get mysterious TypeError at unpickling time—or worse, silently corrupted data.

The bottom line

Python's __reduce__ is one of those features that sits quietly in the documentation, waiting for the moment you desperately need exactly what it does. Understanding it won't change how you write Python every day. But when you encounter an object that simply refuses to be serialized the normal way, you'll remember: there's always __reduce__ waiting in the shadows.

And honestly, that's the beauty of Python's design. The language gives you escape hatches for the impossible cases, trusting that you know when to use them.

Comments

Questions, corrections, and tips stay visible for everyone reading this page.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.