You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Difference in behavior between pure Python pickle and C extension _pickle when unpickling instance of instance of metaclass #105250
When pickling and unpickling an instance of an instance of a metaclass, _pickle reaches into the class object directly and calls tp_new from the C side, whereas pickle uses cls.__new__, which triggers a custom __getattribute__ if defined. Whether this is a bug or just an intentional optimization I'm not sure but at the very least one there is an observable difference in behavior in that unpickling from a distribution where _pickle is available does not call __getattribute__ but unpickling from a distribution where _pickle is not available does call __getattribute__.
To simulate different distributions having _pickle available or not, we can use a meta path entry to cause it to fail to import, causing pickle to fall back to the pure Python version.
importimportlib.abcimportsysclassHideModuleFinder(importlib.abc.MetaPathFinder):
def__init__(self, hidden):
self.hidden=set(hidden)
deffind_spec(self, fullname, path, target=None):
iffullnameinself.hidden:
raiseImportError("Module is hidden")
returnNone# let next finder trydefinstall(hidden):
sys.meta_path.insert(0, HideModuleFinder(hidden))
hide_pickle=False# change me for testing different behaviorifhide_pickle:
install({"_pickle"})
importpickle# must be done after meta path hookclassMeta(type):
def__getattribute__(self, item):
print("__getattribute__ called with", item)
returntype.__getattribute__(self, item)
MyClass=Meta("MyClass", (), {})
obj=MyClass()
print("PICKLING")
obj_str=pickle.dumps(obj)
print("UNPICKLING")
new_obj=pickle.loads(obj_str)
Output with hide_pickle = True
PICKLING
__getattribute__ called with __reduce__
__getattribute__ called with __dict__
__getattribute__ called with __slots__
__getattribute__ called with __new__
__getattribute__ called with __qualname__
__getattribute__ called with __module__
UNPICKLING
__getattribute__ called with __new__
Output with hide_pickle = False
PICKLING
__getattribute__ called with __reduce__
__getattribute__ called with __dict__
__getattribute__ called with __slots__
__getattribute__ called with __qualname__
__getattribute__ called with __module__
UNPICKLING
changed the title [-]Difference in behavior between pure Python pickle and C extension _pickle when unpickling class instances.[/-][+]Difference in behavior between pure Python pickle and C extension _pickle when unpickling instance of instance of metaclass[/+]on Jun 2, 2023
Hi @corona10@hugovk, I'd like to work on this issue as part of the PyCon KR 2026 sprint.
I confirmed it still reproduces on current main (70fdc96).
One subtlety I noticed while investigating: the C unpickler is not always "metaclass-free". load_newobj in Modules/_pickle.c calls tp_new directly (even though the comment above the call says the intent is to call cls.__new__(cls, *args, **kwargs)), but when the class defines __new__ in Python, slot_tp_new itself looks __new__ up on the type, so the metaclass hook does fire even under _pickle. So whether the hook runs currently depends on whether the class being unpickled happens to define __new__ in Python.
Reproducer and output on main
fromtest.support.import_helperimportimport_fresh_modulec_pickle=import_fresh_module("pickle", fresh=["_pickle"])
py_pickle=import_fresh_module("pickle", blocked=["_pickle"])
LOG= []
classMeta(type):
def__getattribute__(cls, name):
LOG.append(name)
returnsuper().__getattribute__(name)
classInheritsNew(metaclass=Meta): # tp_new is object.__new__ (a real C slot)def__init__(self, value):
self.value=valueclassDefinesNew(metaclass=Meta): # tp_new is slot_tp_newdef__new__(cls, *args):
returnsuper().__new__(cls)
def__init__(self, value):
self.value=valuedeflookups(mod, obj, protocol):
"""Attribute lookups the metaclass observes while unpickling."""data=mod.dumps(obj, protocol=protocol)
LOG.clear()
mod.loads(data)
returnsorted(set(LOG))
forclsin (InheritsNew, DefinesNew):
forprotocolin (2, 5):
c=lookups(c_pickle, cls(42), protocol)
py=lookups(py_pickle, cls(42), protocol)
print(f"{cls.__name__} / protocol {protocol}: C={c!s:<13} "f"py={py!s:<13}{'DIVERGES'ifc!=pyelse'same'}")
I'm planning to keep the fix as minimal as possible (mirroring the pure-Python cls.__new__ lookup on the _pickle.c side). If you think this issue isn't a good fit for the sprint, please let me know and I'll pick another one.
Reacted by Donghee Na, Hugo van Kemenade and James Murphy
Activity
[-]Difference in behavior between pure Python pickle and C extension _pickle when unpickling class instances.[/-][+]Difference in behavior between pure Python pickle and C extension _pickle when unpickling instance of instance of metaclass[/+]chaerrypick01 commentedon Aug 16, 2026
Hi @corona10 @hugovk, I'd like to work on this issue as part of the PyCon KR 2026 sprint.
I confirmed it still reproduces on current main (70fdc96).
One subtlety I noticed while investigating: the C unpickler is not always "metaclass-free".
load_newobjinModules/_pickle.ccallstp_newdirectly (even though the comment above the call says the intent is to callcls.__new__(cls, *args, **kwargs)), but when the class defines__new__in Python,slot_tp_newitself looks__new__up on the type, so the metaclass hook does fire even under_pickle. So whether the hook runs currently depends on whether the class being unpickled happens to define__new__in Python.Reproducer and output on main
I'm planning to keep the fix as minimal as possible (mirroring the pure-Python
cls.__new__lookup on the_pickle.cside). If you think this issue isn't a good fit for the sprint, please let me know and I'll pick another one.corona10 commentedon Aug 17, 2026
@chaerrypick01 I think that it's good to go
pythongh-105250: Fix NEWOBJ handling of custom metaclasses in C pickle