GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — gh-105250: Fix NEWOBJ handling of custom metaclasses in C pickle by chaerrypick01 · Pull Request #155920 · python/cpython

Conversation

@chaerrypick01

@chaerrypick01 chaerrypick01 commented Aug 17, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

The NEWOBJ and NEWOBJ_EX opcodes are documented to call cls.__new__(cls, *args), but the C implementation called tp_new directly, so a metaclass __getattribute__ hook was skipped unless the class happened to define __new__ in Python. Perform a real attribute lookup of cls.__new__ when the class has a custom metaclass, matching the pure Python implementation.

When the metaclass is exactly type, no hook can exist and the lookup is not observable, so tp_new is still called directly - no performance change for ordinary classes.

The new test runs against both implementations and covers NEWOBJ (protocols 2-5) and NEWOBJ_EX (protocols 4-5); the cases inheriting object.__new__ fail on the C unpickler without this change.

I benchmarked the three affected paths with pyperf 2.10.0 (script below). Both interpreters were built with the same plain ./configure (release build, Py_DEBUG=0). Baseline is main at 70fdc96. Each benchmark unpickles a
list of 1000 instances.

Benchmark base patched
newobj_default (NEWOBJ, default metaclass) 295 µs 296 µs: not significant
newobj_ex_default (NEWOBJ_EX, default metaclass) 604 µs 605 µs: not significant
newobj_metaclass (NEWOBJ, custom metaclass) 293 µs 338 µs: 1.15x slower

The fast path shows no measurable regression for either opcode — the added Py_TYPE(cls) == &PyType_Type check is lost in the noise. The 1.15x slowdown is confined to the new slow path, which is only taken for classes with a custom metaclass: the cost of the PyObject_GetAttr(cls, '__new__') call plus building the argument tuple, on the path where the previous behaviour did not match the documented semantics.

Environment: macOS 26.5.1, Apple M3 Pro, no CPU isolation (pyperf system tune is Linux-only), 20 processes per benchmark.

Given the trade-off — documented cls.__new__ semantics for custom-metaclass classes, at ~15% on that path and no change for ordinary classes — I'd like your feedback on whether this is acceptable, or whether you'd prefer to close
gh-105250 as a known limitation instead.

Benchmark script
"""Micro-benchmark for load_newobj() in Modules/_pickle.c (gh-105250)."""
import pickle
import pyperf

assert pickle.Unpickler.__module__ == '_pickle', 'need the C implementation'


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


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

    def __getnewargs_ex__(self):
        return (self.x,), {'y': self.y}

    def __new__(cls, x=0, y=0):
        return super().__new__(cls)


class Meta(type):
    pass


class PointMeta(metaclass=Meta):
    def __init__(self, x, y):
        self.x = x
        self.y = y


N = 1000
DATA_DEFAULT = pickle.dumps([Point(i, i) for i in range(N)], protocol=2)
DATA_EX = pickle.dumps([PointEx(i, i) for i in range(N)], protocol=4)
DATA_META = pickle.dumps([PointMeta(i, i) for i in range(N)], protocol=2)

runner = pyperf.Runner()
runner.metadata['description'] = 'unpickle NEWOBJ/NEWOBJ_EX (gh-105250)'
runner.bench_func('newobj_default', pickle.loads, DATA_DEFAULT)
runner.bench_func('newobj_ex_default', pickle.loads, DATA_EX)
runner.bench_func('newobj_metaclass', pickle.loads, DATA_META)
  • Issue: Difference in behavior between pure Python pickle and C extension _pickle when unpickling instance of instance of metaclass #105250

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@chaerrypick01
pythongh-105250: Fix NEWOBJ handling of custom metaclasses in C pickle
10399f7
The NEWOBJ and NEWOBJ_EX opcodes are documented to call
cls.__new__(cls, *args), but the C implementation called tp_new
directly, so a metaclass __getattribute__ hook was skipped unless the
class happened to define __new__ in Python.  Perform a real attribute
lookup of cls.__new__ when the class has a custom metaclass, matching
the pure Python implementation.  The default-metaclass case keeps
calling tp_new directly, where the lookup is not observable.
@python-cla-bot

python-cla-bot Bot commented Aug 17, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

All commit authors signed the Contributor License Agreement.

CLA signed

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@bedevere-app bedevere-app Bot mentioned this pull request Aug 17, 2026
Difference in behavior between pure Python pickle and C extension _pickle when unpickling instance of instance of metaclass #105250
Open
@bedevere-app bedevere-app Bot added the awaiting review label Aug 17, 2026
corona10
corona10 reviewed Aug 17, 2026
View reviewed changes
Comment thread Misc/NEWS.d/next/Library/2026-08-17-11-35-48.gh-issue-105250.wNaXOE.rst Resolved

Uh oh!

There was an error while loading. Please reload this page.

corona10
corona10 reviewed Aug 17, 2026
View reviewed changes
Comment thread Lib/test/picklecommon.py
metaclass_new_lookups = []

class LookupLoggingMeta(type):
def __getattribute__(cls, name):

@corona10 corona10 Aug 17, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to move look up count as class varaible
Something like:

class LookupLoggingMeta(type):
     new_lookup_count = 0
    def __getattribute__(cls, name):

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@chaerrypick01 chaerrypick01 Aug 17, 2026

Copy link
Copy Markdown
Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in f22a1a5 — moved the count to a class variable on the metaclass.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@corona10 corona10 added the sprint label Aug 17, 2026
@github-project-automation github-project-automation Bot added this to Sprint Aug 17, 2026
@github-project-automation github-project-automation Bot moved this to Todo in Sprint Aug 17, 2026
@corona10

corona10 commented Aug 17, 2026

Copy link
Copy Markdown
Member

Can you also provide benchmark compare to main branch by using pyperf?

chaerrypick01 reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

chaerrypick01 and others added 2 commits August 17, 2026 12:48
@chaerrypick01
Track __new__ lookups via a class variable on the metaclass
f22a1a5
@chaerrypick01 @corona10
Update Misc/NEWS.d/next/Library/2026-08-17-11-35-48.gh-issue-105250.w…
25924ec
…NaXOE.rst

Co-authored-by: Donghee Na <[email protected]>
corona10
corona10 reviewed Aug 17, 2026
View reviewed changes
Comment thread Lib/test/picklecommon.py Outdated

def __getattribute__(cls, name):
if name == '__new__':
LookupLoggingMeta.new_lookup_count += 1

@corona10 corona10 Aug 17, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
LookupLoggingMeta.new_lookup_count += 1
cls.new_lookup_count += 1

?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@chaerrypick01
Record the last __new__ lookup by class name instead of counting
25eb25a
@corona10
corona10 requested a review from serhiy-storchaka August 17, 2026 06:16
corona10
corona10 reviewed Aug 17, 2026
View reviewed changes
Comment thread Modules/_pickle.c Outdated
Comment on lines +6339 to +6358
/* Look __new__ up on the class so that a custom metaclass
__getattribute__ observes the lookup, as in the Python
implementation. */
PyObject *func = PyObject_GetAttr(cls, &_Py_ID(__new__));
if (func == NULL) {
goto error;
}
Py_ssize_t nargs = PyTuple_GET_SIZE(args);
PyObject *newargs = PyTuple_New(nargs + 1);
if (newargs == NULL) {
Py_DECREF(func);
goto error;
}
PyTuple_SET_ITEM(newargs, 0, Py_NewRef(cls));
for (Py_ssize_t i = 0; i < nargs; i++) {
PyTuple_SET_ITEM(newargs, i + 1,
Py_NewRef(PyTuple_GET_ITEM(args, i)));
}
obj = PyObject_Call(func, newargs, kwargs);
Py_DECREF(newargs);

@corona10 corona10 Aug 17, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/* Look __new__ up on the class so that a custom metaclass
__getattribute__ observes the lookup, as in the Python
implementation. */
PyObject *func = PyObject_GetAttr(cls, &_Py_ID(__new__));
if (func == NULL) {
goto error;
}
Py_ssize_t nargs = PyTuple_GET_SIZE(args);
PyObject *newargs = PyTuple_New(nargs + 1);
if (newargs == NULL) {
Py_DECREF(func);
goto error;
}
PyTuple_SET_ITEM(newargs, 0, Py_NewRef(cls));
for (Py_ssize_t i = 0; i < nargs; i++) {
PyTuple_SET_ITEM(newargs, i + 1,
Py_NewRef(PyTuple_GET_ITEM(args, i)));
}
obj = PyObject_Call(func, newargs, kwargs);
Py_DECREF(newargs);
PyThreadState *tstate = _PyThreadState_GET();
obj = _PyObject_Call_Prepend(tstate, func, cls, args, kwargs);

Can you benchmark this one too?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@chaerrypick01 chaerrypick01 Aug 17, 2026

Copy link
Copy Markdown
Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I applied it(b4dfe45), and re-ran the benchmark to see whether the small-stack call path helps:
it doesn't, measurably. newobj_metaclass stays at ~1.15x (337 µs vs 294 µs baseline, within run-to-run noise of the tuple version's 338 µs), and the fast paths are unchanged. So this commit is a code simplification and a consistency win, not a performance change — the numbers in the benchmark comment above still stand.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

corona10 reacted with thumbs up emoji
All reactions
  • 👍 1 reaction
@chaerrypick01
Use _PyObject_Call_Prepend() in load_newobj()
b4dfe45
Export it from pycore_call.h, since _pickle can be built as a shared
extension and cannot link against a hidden internal symbol.
corona10
corona10 approved these changes Aug 17, 2026
View reviewed changes

@corona10 corona10 left a comment •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@serhiy-storchaka

I think the behavior change looks correct, but this change does introduce some performance overhead.
I’m fine with approving this PR, though I’m not sure whether you’ll be satisfied with the performance impact. :)

FYI, @chaerrypick01 is a first-time contributor who participated in the PyCon KR sprint organized by @hugovk and me. :)

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@bedevere-app bedevere-app Bot added awaiting merge and removed awaiting review labels Aug 17, 2026
@github-project-automation github-project-automation Bot moved this from Todo to In Progress in Sprint Aug 17, 2026
@corona10 corona10 assigned serhiy-storchaka Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@corona10 corona10 corona10 approved these changes
@serhiy-storchaka serhiy-storchaka Awaiting requested review from serhiy-storchaka

Uh oh!

There was an error while loading. Please reload this page.

Assignees

@serhiy-storchaka serhiy-storchaka

Labels

awaiting merge sprint

Projects

Sprint
Status: In Progress

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

3 participants

@chaerrypick01 @corona10 @serhiy-storchaka
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.