Skip to content

gh-87613: Pass NULL for an empty **kwargs in Argument Clinic - #161382

Open
cmaloney wants to merge 1 commit into
python:mainfrom
cmaloney:clinic_kwargs_null_pr
Open

cmaloney wants to merge 1 commit into
python:mainfrom
cmaloney:clinic_kwargs_null_pr

Conversation

@cmaloney

@cmaloney cmaloney commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

The generated parser allocated an empty dict for **kwargs when there were no keyword arguments. Pass NULL instead, like the caller does, and handle NULL in the impls that used the dict unconditionally.

This is preparation work for supporting **kwargs in Argument Clinic Vectorcall where allocating a dict instead of passing NULL makes a significant performance impact.

All **kwargs impls were audited: _csv and zoneinfo already accept NULL, and the rest (structseq and SimpleNamespace __replace__, _interpreters.new_config, atexit.register, SubElement) now check for it. atexit stores None, which the run loop already maps back to NULL.

pyperf, PGO+LTO, t = time.gmtime(0), ns = SimpleNamespace(a=1):

Benchmark main this change
struct_time.__replace__() 229 ns 47.0 ns: 4.87x faster
SimpleNamespace.__replace__() 72.2 ns 65.5 ns: 1.10x faster

The parser allocated an empty dict for **kwargs when there were no
keyword arguments.  Pass NULL instead, like the caller does, and handle
NULL in the impls that used the dict unconditionally.

All **kwargs impls were audited: _csv and zoneinfo already accept NULL,
and the rest (structseq and SimpleNamespace `__replace__`,
_interpreters.new_config, atexit.register, SubElement) now check for it.
atexit stores None, which the run loop already maps back to NULL.

pyperf, PGO+LTO, `t = time.gmtime(0)`, `ns = SimpleNamespace(a=1)`:

| Benchmark                       | main   | this change           |
|---------------------------------|:------:|:---------------------:|
| struct_time.`__replace__`()     | 229 ns | 47.0 ns: 4.87x faster |
| SimpleNamespace.`__replace__`() | 72.2 ns | 65.5 ns: 1.10x faster |

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment on lines +1452 to +1453
# NULL when there are no keywords: no call allocates an empty dict.
return f"{self.parser_name} = Py_XNewRef(kwargs);\n"

@msullivan msullivan Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is there anywhere to document the new behavior, to make sure this isn't a footgun?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

https://devguide.python.org/development-tools/clinic/ is all the docs and doesn't go into that much detail. For https://docs.python.org/3.13/c-api/call.html#c.vectorcallfunc it is documented as able to be NULL, so this to me is bringing AC parsers into that same state.

@cmaloney

cmaloney commented Nov 6, 2026

Copy link
Copy Markdown
Contributor Author

@eendebakpt could you take a look at this one?

This + the "support **kwargs in AC" that comes after enable using AC vectorcall for a speedup on most objects, in particular BaseException. It also should enable converting dict and frozendict from handwritten.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants