skip to content

__reduce__ and copyreg

__reduce__ hands the pickler a callable plus its arguments to rebuild the object, and copyreg registers that recipe for a type you do not own. Interviewers ask how a singleton survives a round trip.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

4

What must a class's `__reduce__` return, and how does `pickle` use it to rebuild the object?

level: middleimportance: must knowfreq 40%

answer

  1. Rebuild instructions, not a memory copy
  2. Two things: what to call, with what
  3. A string return means look up a global
  4. The callable is stored by name
  5. `__reduce_ex__` comes first and delegates

basics

~20 s

reduce returns either a string naming a module-level global to look up, or a tuple whose first item is a callable and whose second is a tuple of arguments. Loading imports that callable by name and calls it with those arguments.

solid answer

~50 s

`__reduce__` describes how to *rebuild* the object, not how to copy its bytes. The usual form is a 2-to-6 item tuple: a callable, a tuple of positional arguments for it, then optional state and iterator items. On load, `pickle` resolves the callable by its module-qualified name, calls it with the unpickled arguments, and applies whatever else the tuple carried. The callable must itself be picklable, so it has to be a module-level function or a class in an importable module — a lambda or a closure cannot be used. `__reduce__` may also return a plain string, which means "look up the global of this name in the object's module"; that is how a sentinel or singleton keeps its identity through a round trip. `pickle` actually calls `__reduce_ex__(protocol)`, whose `object` implementation delegates to a user-defined `__reduce__`.

code

python · 10 lines
python
import pickle

class Temp:
    def __init__(self, celsius):
        self.celsius = celsius

    def __reduce__(self):
        return (Temp, (self.celsius,))

print(pickle.loads(pickle.dumps(Temp(21.5))).celsius)

go deeper

for a junior

Be ready to say that pickling writes instructions for rebuilding an object rather than a copy of its memory, and that __reduce__ supplies those instructions as a callable and its arguments.

for a middle

Explain the tuple item by item, why the callable has to be a module-level function or class, and what returning a plain string does. Show a round trip you have written yourself.

for a senior

Demonstrate judgment about what belongs in the arguments: small, immutable, reconstructible parameters rather than live handles. Be able to diagnose a pool failure back to a reduce recipe that named an unpicklable callable.

for a principal

Own the question of whether an object should be reducible at all. A reduce recipe is executable content and a long-lived compatibility surface, so decide where a documented data format beats a language-specific hook.

### What the reduce protocol actually is Pickling never copies an object's memory. It writes a small program of opcodes that, when replayed, *reconstructs* an equivalent object. The reduce protocol is how an object dictates that program: it hands back a callable plus the arguments needed to call it. Think of it as posting a recipe rather than a photograph. The entry point is `__reduce_ex__(protocol)`. The pickler calls that, never `__reduce__` directly. `object.__reduce_ex__` looks at whether the class overrides `__reduce__`; if it does, it calls it and uses the result, and if it does not, it builds a default reduction itself using helpers from `copyreg`. So writing `__reduce__` is enough for almost every case; you override `__reduce_ex__` only when the recipe genuinely has to differ per protocol version. ### The tuple The return value is either a string or a tuple of two to six items: 1. **A callable.** Called at load time to produce the object. 2. **A tuple of arguments** for that callable. It may be empty, but it must be a tuple. 3. *(optional)* **State**, handed to the rebuilt object's restore step. 4. *(optional)* **An iterator of items** appended to the rebuilt object, for list-like types. 5. *(optional)* **An iterator of key/value pairs** set on the rebuilt object, for dict-like types. 6. *(optional)* **A callable** used instead of the default way of applying the state. Most hand-written reductions use only the first two. The list and dict iterator slots exist so a huge container can be streamed in pieces instead of materialised as one giant argument tuple. ### How the callable is stored This is where most interview follow-ups go. The callable is itself pickled, and functions and classes are pickled **by reference**: `pickle` writes the callable's module name and qualified name and looks that pair up again on load. Two consequences fall straight out of that. First, the callable has to be reachable at module level in an importable module — a `lambda`, a function defined inside another function, or a class built at runtime all fail with `pickle.PicklingError` because the lookup cannot resolve them. Second, the arguments in the tuple are pickled recursively, so they must be picklable too, and they are the wrong place to put an open file, a socket or a lock. ### The string form If `__reduce__` returns a plain string, `pickle` treats it as the name of a global to look up in the object's module at load time, and stores no state at all. That is the idiomatic way to make a sentinel survive a round trip with its identity intact: ```python class _Missing: def __reduce__(self): return "MISSING" MISSING = _Missing() ``` After `pickle.loads(pickle.dumps(MISSING))`, the result **is** `MISSING`, so `is` comparisons in the loading process still work. The pickler verifies that the name really resolves back to the object being pickled and raises `pickle.PicklingError` if it does not, which catches the case where the global was renamed. ### Who else uses it The reduce protocol is not only for files on disk. Anything that moves objects between processes goes through it: sending arguments to a worker in a process pool serialises them the same way, so a "this cannot be pickled" error in a pool is a reduce-protocol error. Because of that, `__reduce__` is worth reaching for whenever an object is *reconstructible from a few parameters* — a connection described by a URL, a compiled pattern described by its source, a handle described by the identifier it was opened with. ### Practical cautions Keep the reduce callable boring: a module-level factory function or the class itself. Keep the arguments small and immutable — they are the payload, and anything huge in them is written into every pickle. Remember that the callable runs at load time with whatever arguments the payload contains, so a reduce recipe is executable content; that is the reason loading data you did not produce is dangerous, which is a separate discussion from writing the hook. Finally, if you want to see what your hook actually produced, disassemble the bytes with `pickletools.dis` rather than guessing from the repr. ### Versions The protocol itself has been stable for years, but the default protocol has not: `pickle.DEFAULT_PROTOCOL` is 5 on Python 3.14, and was 4 from 3.8 through 3.13. Protocol only affects the *encoding* and which default reduction path is used, not the shape of the tuple you return.

  • What does it mean when `__reduce__` returns a plain string rather than a tuple?
    It names a global to look up in the object's module at load time, and no state is written. It is the sentinel and singleton recipe: the loaded object is the same object the module already holds, so identity checks still pass. The pickler verifies at dump time that the name really resolves to the object, and raises `pickle.PicklingError` if the global has been renamed or is missing.
  • Why must the callable in the reduce tuple be picklable, and what actually qualifies?
    The callable is serialised along with the arguments, and functions and classes are serialised by module name plus qualified name rather than by code. So a module-level function, or the class itself, qualifies; a lambda, a closure, or anything defined inside another function does not, because the name lookup cannot find it again on load.
  • Does `pickle` call `__reduce__` or `__reduce_ex__`, and when would you override the latter?
    It calls `__reduce_ex__(protocol)`. The implementation on `object` delegates to a user-defined `__reduce__` when the class defines one, and otherwise builds the default reduction itself. Override `__reduce_ex__` only when the recipe must vary by protocol version — for example emitting an out-of-band buffer form under protocol 5 and a plain byte payload under older protocols.

A pickle is a recipe, not a photograph: __reduce__ writes down the chef to call and the ingredients to hand them, and loading simply cooks the dish again.

saying these in an interview costs you the question

  • Thinks pickle stores the object's memory or the class's bytecode
  • Says `__reduce__` should return the instance `__dict__`
  • Returns a lambda as the reduce callable
  • Forgets the second element must be a tuple of arguments
  • Assumes the reduce arguments are exempt from being pickled
  • Believes a user `__reduce__` is ignored unless `__reduce_ex__` is written too

context

open as a page

Why does `pickle.dumps` reject a lambda but accept a module-level function?

level: juniorimportance: should knowfreq 45%

basics

~20 s

pickle does not serialise a function's code. It stores the function's module name and qualified name and looks that pair up again on load. A lambda's qualified name is <lambda>, which resolves to nothing, so dumping raises pickle.PicklingError.

open as a page

How does `copyreg.pickle` make a type you do not own picklable, when an ETL export must ship it through a process pool?

level: seniorimportance: should knowfreq 30%

basics

~10 s

copyreg.pickle(SomeType, reduce_func) registers a reduction function for a type whose source you cannot change. It stores the function in copyreg.dispatch_table, and the pickler consults that table for objects of exactly that type.

open as a page

Why does a `tuple` subclass with a custom `__new__` fail to unpickle, and what fixes it?

level: middleimportance: nice to knowfreq 16%

basics

~20 s

Loading rebuilds the instance by calling cls.new(cls, *args), where args come from getnewargs. A tuple subclass inherits one that returns the whole tuple, so a two-parameter new receives a single argument and raises TypeError. Define getnewargs to match new.

open as a page