Skip to content

gh-150816: Speed up inspect.signature() for Python functions#150823

Open
gaborbernat wants to merge 2 commits into
python:mainfrom
gaborbernat:opt/inspect-signature-fast-param
Open

gh-150816: Speed up inspect.signature() for Python functions#150823
gaborbernat wants to merge 2 commits into
python:mainfrom
gaborbernat:opt/inspect-signature-fast-param

Conversation

@gaborbernat
Copy link
Copy Markdown
Contributor

@gaborbernat gaborbernat commented Jun 2, 2026

inspect.signature() builds a Signature whose every Parameter goes through the public Parameter constructor, which validates the name and the kind on each call. When the parameters come from a function's own code object, the names are already valid identifiers and the kinds are already the canonical constants, so that validation re-checks facts that are guaranteed to hold. Signature introspection runs constantly across the ecosystem — web frameworks resolving view and dependency arguments, click building commands, pytest wiring fixtures, serialization and validation layers — usually once per function and often at import time while an application is assembled.

This adds an internal Parameter._from_valid_args() used only by _signature_from_function, the trusted caller that reads parameters from a code object. It populates the instance directly and skips the redundant checks. The one case that still needs the constructor's handling, a comprehension's implicit .0 argument, is detected and deferred to it. The public Parameter() constructor and all of its validation are untouched, so every other caller behaves exactly as before.

Taking the signature of 400 real callables collected from popular packages (requests, click, jinja2, sqlalchemy, werkzeug, flask and others) improves from 1.46 ms to 1.16 ms, 26% faster.

Benchmark base patched
inspect.signature over 400 real callables 1.46 ms 1.16 ms: 26% faster
Benchmark (pyperf)

Run base vs patched by swapping Lib/inspect.py on the same interpreter. The script collects callables from installed third-party packages when present and always includes several stdlib modules, so it runs with no extra installs.

import inspect, importlib, pyperf

names = ["requests", "click", "jinja2", "sqlalchemy", "werkzeug", "flask",
         "urllib3", "rich.console", "json", "argparse", "logging", "http.client",
         "email.message", "configparser", "xml.etree.ElementTree", "subprocess"]
funcs = []
for name in names:
    try:
        mod = importlib.import_module(name)
    except ImportError:
        continue
    for attr in dir(mod):
        obj = getattr(mod, attr, None)
        if inspect.isfunction(obj):
            funcs.append(obj)
        elif inspect.isclass(obj):
            funcs.extend(m for m in vars(obj).values() if inspect.isfunction(m))
good = []
for f in funcs:
    try:
        inspect.signature(f); good.append(f)
    except (ValueError, TypeError):
        pass
good = good[:400]

runner = pyperf.Runner()
runner.metadata["n_callables"] = len(good)
runner.bench_func("inspect.signature over %d callables" % len(good),
                  lambda: [inspect.signature(f) for f in good])

Resolves #150816.

@skirpichev
Copy link
Copy Markdown
Member

In order to keep the commit history intact, please avoid squashing or amending history and then force-pushing to the PR. Reviewers often want to look at individual commits. When the PR is merged, everything will be squashed into a single commit.

Copy link
Copy Markdown
Member

@skirpichev skirpichev left a comment

Choose a reason for hiding this comment

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

Looks good, with few comments.

CC @sobolevn

Comment thread Lib/inspect.py Outdated
Comment thread Lib/inspect.py
Parameters built from a function's code object always have valid identifier
names and canonical kinds, yet each one is constructed through the validating
Parameter() constructor. Add Parameter._from_valid_args() for these trusted
callers to skip the redundant checks, inlining the comprehension implicit-arg
recast ('.N' -> 'implicitN', positional-only) that __init__ performs. The
public constructor and its validation are unchanged.
@gaborbernat gaborbernat force-pushed the opt/inspect-signature-fast-param branch from fafbd85 to eb05e27 Compare June 3, 2026 13:36
The inlined recast and deferring to __init__ for the rare '.0'
comprehension argument perform identically, so keep the simpler version
that does not duplicate __init__'s logic.
@gaborbernat
Copy link
Copy Markdown
Contributor Author

Good point — I benchmarked both: inlining the recast and deferring the rare .0 comprehension case to __init__ perform identically, so I've kept the simpler version that doesn't duplicate __init__'s logic, as you suggested.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Speed up inspect.signature() for Python functions

4 participants