From 78329d0dc79dfd017913284ae8624fca1a70dec4 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Sun, 13 Sep 2026 21:36:41 +0200 Subject: [PATCH 1/2] Speed up inspect.signature() for Python functions For real Python functions the parameter kinds and defaults come straight from the code object, so the validation done by Parameter.__init__ is redundant. Construct the Parameter objects directly, falling back to the regular constructor for names that are not plain identifiers (e.g. the ".0" implicit argument of comprehensions). Parameter subclasses and duck-typed functions keep the full validation. inspect.signature() on a function with 7 parameters: 23.4 us -> 14.5 us. Co-Authored-By: Claude Opus 5 (1M context) --- Lib/inspect.py | 21 +++++++++++++++++++++ Lib/test/test_inspect/test_inspect.py | 14 ++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/Lib/inspect.py b/Lib/inspect.py index 3683f8c3fd53308..e4e2af877388171 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2348,6 +2348,8 @@ def _signature_from_function(cls, func, skip_bound_arg=True, return _signature_fromstr(cls, func, s, skip_bound_arg) Parameter = cls._parameter_cls + if Parameter is _Parameter and not is_duck_function: + Parameter = _parameter_from_code # Parameter information. func_code = func.__code__ @@ -2835,6 +2837,25 @@ def __eq__(self, other): self._annotation == other._annotation) +_Parameter = Parameter + + +def _parameter_from_code(name, kind, *, default=_empty, annotation=_empty): + """Private helper: fast Parameter construction for Python functions. + + The kind and default are taken from the function itself and are + known to be valid, so only the name has to be checked. + """ + if iskeyword(name) or not name.isidentifier(): + return _Parameter(name, kind, default=default, annotation=annotation) + self = object.__new__(_Parameter) + self._name = name + self._kind = kind + self._default = default + self._annotation = annotation + return self + + class BoundArguments: """Result of `Signature.bind` call. Holds the mapping of arguments to the function's parameters. diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index df5843abfcb8753..5f9d6e7d7cd277e 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -5656,6 +5656,20 @@ def test_signature_parameter_implicit(self): self.assertEqual(param.kind, inspect.Parameter.POSITIONAL_ONLY) self.assertEqual(param.name, 'implicit0') + @cpython_only + def test_signature_from_code_unusual_names(self): + def f(a, b): pass + f.__code__ = f.__code__.replace(co_varnames=('.0', 'b')) + sig = inspect.signature(f) + self.assertEqual(list(sig.parameters), ['implicit0', 'b']) + self.assertEqual(sig.parameters['implicit0'].kind, + inspect.Parameter.POSITIONAL_ONLY) + + f.__code__ = f.__code__.replace(co_varnames=('if', 'b')) + with self.assertRaisesRegex(ValueError, + 'is not a valid parameter name'): + inspect.signature(f) + def test_signature_parameter_immutability(self): p = inspect.Parameter('spam', kind=inspect.Parameter.KEYWORD_ONLY) From dc3d4ffe5504243d4205c73ff9062caa85edbec5 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Sun, 13 Sep 2026 22:25:10 +0200 Subject: [PATCH 2/2] Make the fast Parameter constructor a classmethod and add tests Move the helper to Parameter._from_code(). Add tests that a Signature subclass overriding _parameter_cls still gets its own Parameter class (the concern raised on gh-150823), and that parameter names of function-like objects are still validated. Co-Authored-By: Claude Opus 5 (1M context) --- Lib/inspect.py | 35 +++++++--------- Lib/test/test_inspect/test_inspect.py | 42 +++++++++++++++++++ ...-09-13-21-45-00.gh-issue-150816.pQ3kZs.rst | 2 + 3 files changed, 58 insertions(+), 21 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-13-21-45-00.gh-issue-150816.pQ3kZs.rst diff --git a/Lib/inspect.py b/Lib/inspect.py index e4e2af877388171..bceed35a53abba1 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2348,8 +2348,8 @@ def _signature_from_function(cls, func, skip_bound_arg=True, return _signature_fromstr(cls, func, s, skip_bound_arg) Parameter = cls._parameter_cls - if Parameter is _Parameter and not is_duck_function: - Parameter = _parameter_from_code + if Parameter is Signature._parameter_cls and not is_duck_function: + Parameter = Parameter._from_code # Parameter information. func_code = func.__code__ @@ -2748,6 +2748,18 @@ def __init__(self, name, kind, *, default=_empty, annotation=_empty): self._name = name + @classmethod + def _from_code(cls, name, kind, *, default=_empty, annotation=_empty): + # Fast path for Python functions: only the name needs validation. + if iskeyword(name) or not name.isidentifier(): + return cls(name, kind, default=default, annotation=annotation) + self = object.__new__(cls) + self._name = name + self._kind = kind + self._default = default + self._annotation = annotation + return self + def __reduce__(self): return (type(self), (self._name, self._kind), @@ -2837,25 +2849,6 @@ def __eq__(self, other): self._annotation == other._annotation) -_Parameter = Parameter - - -def _parameter_from_code(name, kind, *, default=_empty, annotation=_empty): - """Private helper: fast Parameter construction for Python functions. - - The kind and default are taken from the function itself and are - known to be valid, so only the name has to be checked. - """ - if iskeyword(name) or not name.isidentifier(): - return _Parameter(name, kind, default=default, annotation=annotation) - self = object.__new__(_Parameter) - self._name = name - self._kind = kind - self._default = default - self._annotation = annotation - return self - - class BoundArguments: """Result of `Signature.bind` call. Holds the mapping of arguments to the function's parameters. diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 5f9d6e7d7cd277e..7c6f3324af94c2e 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -3556,6 +3556,48 @@ def __init__(self, marker): self.assertEqual(str(inspect.signature(funclike)), '(marker)') + @cpython_only + def test_signature_functionlike_invalid_names(self): + # The code object of a function-like object is not guaranteed + # to have valid parameter names, so they must be validated. + def func(a, b): + pass + + class funclike: + __name__ = func.__name__ + __code__ = func.__code__.replace(co_varnames=('a', '$b')) + __annotations__ = {} + __defaults__ = None + __kwdefaults__ = None + + def __call__(self, *args): + pass + + with self.assertRaisesRegex(ValueError, + 'is not a valid parameter name'): + inspect.signature(funclike()) + + def test_signature_parameter_cls_subclass(self): + # A Signature subclass can override _parameter_cls with a + # Parameter subclass that has its own constructor. + class MyParameter(inspect.Parameter): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.extra = 'spam' + + class MySignature(inspect.Signature): + _parameter_cls = MyParameter + + def f(a, /, b=1, *args, c, d=2, **kwargs): + pass + + sig = MySignature.from_callable(f) + self.assertEqual(len(sig.parameters), 6) + for param in sig.parameters.values(): + self.assertIs(type(param), MyParameter) + self.assertEqual(param.extra, 'spam') + self.assertEqual(sig, inspect.signature(f)) + def test_signature_on_method(self): class Test: def __init__(*args): diff --git a/Misc/NEWS.d/next/Library/2026-09-13-21-45-00.gh-issue-150816.pQ3kZs.rst b/Misc/NEWS.d/next/Library/2026-09-13-21-45-00.gh-issue-150816.pQ3kZs.rst new file mode 100644 index 000000000000000..4f67195f45fc6dc --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-13-21-45-00.gh-issue-150816.pQ3kZs.rst @@ -0,0 +1,2 @@ +Speed up :func:`inspect.signature` for Python functions by skipping +redundant validation when creating :class:`inspect.Parameter` objects.