From eb05e276796b8e1fbcb85989ff7e59f679da9ef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Mon, 1 Jun 2026 16:51:17 -0700 Subject: [PATCH 1/3] gh-150816: Speed up inspect.signature() for Python functions 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. --- Lib/inspect.py | 36 ++++++++++++------- ...-06-02-15-44-56.gh-issue-150816.qBXOBw.rst | 3 ++ 2 files changed, 27 insertions(+), 12 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-06-02-15-44-56.gh-issue-150816.qBXOBw.rst diff --git a/Lib/inspect.py b/Lib/inspect.py index af6aa3eb37a53b..a369fc782d1049 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2366,8 +2366,7 @@ def _signature_from_function(cls, func, skip_bound_arg=True, for name in positional[:non_default_count]: kind = _POSITIONAL_ONLY if posonly_left else _POSITIONAL_OR_KEYWORD annotation = annotations.get(name, _empty) - parameters.append(Parameter(name, annotation=annotation, - kind=kind)) + parameters.append(Parameter._from_valid_args(name, kind, _empty, annotation)) if posonly_left: posonly_left -= 1 @@ -2375,9 +2374,7 @@ def _signature_from_function(cls, func, skip_bound_arg=True, for offset, name in enumerate(positional[non_default_count:]): kind = _POSITIONAL_ONLY if posonly_left else _POSITIONAL_OR_KEYWORD annotation = annotations.get(name, _empty) - parameters.append(Parameter(name, annotation=annotation, - kind=kind, - default=defaults[offset])) + parameters.append(Parameter._from_valid_args(name, kind, defaults[offset], annotation)) if posonly_left: posonly_left -= 1 @@ -2385,8 +2382,7 @@ def _signature_from_function(cls, func, skip_bound_arg=True, if func_code.co_flags & CO_VARARGS: name = arg_names[pos_count + keyword_only_count] annotation = annotations.get(name, _empty) - parameters.append(Parameter(name, annotation=annotation, - kind=_VAR_POSITIONAL)) + parameters.append(Parameter._from_valid_args(name, _VAR_POSITIONAL, _empty, annotation)) # Keyword-only parameters. for name in keyword_only: @@ -2395,9 +2391,7 @@ def _signature_from_function(cls, func, skip_bound_arg=True, default = kwdefaults.get(name, _empty) annotation = annotations.get(name, _empty) - parameters.append(Parameter(name, annotation=annotation, - kind=_KEYWORD_ONLY, - default=default)) + parameters.append(Parameter._from_valid_args(name, _KEYWORD_ONLY, default, annotation)) # **kwargs if func_code.co_flags & CO_VARKEYWORDS: index = pos_count + keyword_only_count @@ -2406,8 +2400,7 @@ def _signature_from_function(cls, func, skip_bound_arg=True, name = arg_names[index] annotation = annotations.get(name, _empty) - parameters.append(Parameter(name, annotation=annotation, - kind=_VAR_KEYWORD)) + parameters.append(Parameter._from_valid_args(name, _VAR_KEYWORD, _empty, annotation)) # Is 'func' is a pure Python function - don't validate the # parameters list (for correct order and defaults), it should be OK. @@ -2736,6 +2729,25 @@ def __init__(self, name, kind, *, default=_empty, annotation=_empty): self._name = name + @classmethod + def _from_valid_args(cls, name, kind, default, annotation): + # Fast path for trusted callers (e.g. _signature_from_function), where + # the name comes from a code object's co_varnames -- always a valid, + # non-keyword identifier -- and the kind is one of the module-level + # _ParameterKind constants. Skips the validation done in __init__. + if name[0] == '.' and name[1:].isdigit(): + # Implicit argument generated by a comprehension; recast '.N' as + # 'implicitN' and treat it as positional-only, as __init__ does + # (see gh-issue 19611). + kind = _POSITIONAL_ONLY + name = 'implicit' + name[1:] + self = cls.__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), diff --git a/Misc/NEWS.d/next/Library/2026-06-02-15-44-56.gh-issue-150816.qBXOBw.rst b/Misc/NEWS.d/next/Library/2026-06-02-15-44-56.gh-issue-150816.qBXOBw.rst new file mode 100644 index 00000000000000..cb71f807f28bc7 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-02-15-44-56.gh-issue-150816.qBXOBw.rst @@ -0,0 +1,3 @@ +Speed up :func:`inspect.signature` for Python functions by skipping redundant +:class:`inspect.Parameter` validation when the parameters are built from a +function's own code object. Patch by Bernát Gábor. From 667c5641f64659e2910565844f0751b6d458c706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Wed, 3 Jun 2026 07:01:22 -0700 Subject: [PATCH 2/3] Defer comprehension implicit-arg recast to __init__ 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. --- Lib/inspect.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index a369fc782d1049..fced964a71400e 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2735,12 +2735,11 @@ def _from_valid_args(cls, name, kind, default, annotation): # the name comes from a code object's co_varnames -- always a valid, # non-keyword identifier -- and the kind is one of the module-level # _ParameterKind constants. Skips the validation done in __init__. - if name[0] == '.' and name[1:].isdigit(): - # Implicit argument generated by a comprehension; recast '.N' as - # 'implicitN' and treat it as positional-only, as __init__ does - # (see gh-issue 19611). - kind = _POSITIONAL_ONLY - name = 'implicit' + name[1:] + # Implicit comprehension arguments ('.0') are rare and need the + # recast __init__ performs, so defer those to it rather than + # duplicate the logic here. + if name[0] == '.': + return cls(name, kind=kind, default=default, annotation=annotation) self = cls.__new__(cls) self._name = name self._kind = kind From a567bd1fe1eddc7754fe924299c787d5662458f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Wed, 3 Jun 2026 12:41:52 -0700 Subject: [PATCH 3/3] Validate parameters from function-like (duck) objects _signature_from_function() also handles third-party function-like objects (is_duck_function), whose code object's co_varnames are not guaranteed to be valid identifiers. Route those through the validating Parameter() constructor; keep the _from_valid_args fast path only for real Python functions. --- Lib/inspect.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index fced964a71400e..5d7330172c1edc 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2339,6 +2339,17 @@ def _signature_from_function(cls, func, skip_bound_arg=True, Parameter = cls._parameter_cls + # A real Python function has valid identifier names in co_varnames, so its + # parameters can be built without re-validating them. A function-like + # object (is_duck_function) carries an arbitrary code object whose + # co_varnames are not guaranteed to be valid identifiers, so it must go + # through the validating constructor. + if is_duck_function: + def make_param(name, kind, default, annotation, _P=Parameter): + return _P(name, kind, default=default, annotation=annotation) + else: + make_param = Parameter._from_valid_args + # Parameter information. func_code = func.__code__ pos_count = func_code.co_argcount @@ -2366,7 +2377,7 @@ def _signature_from_function(cls, func, skip_bound_arg=True, for name in positional[:non_default_count]: kind = _POSITIONAL_ONLY if posonly_left else _POSITIONAL_OR_KEYWORD annotation = annotations.get(name, _empty) - parameters.append(Parameter._from_valid_args(name, kind, _empty, annotation)) + parameters.append(make_param(name, kind, _empty, annotation)) if posonly_left: posonly_left -= 1 @@ -2374,7 +2385,7 @@ def _signature_from_function(cls, func, skip_bound_arg=True, for offset, name in enumerate(positional[non_default_count:]): kind = _POSITIONAL_ONLY if posonly_left else _POSITIONAL_OR_KEYWORD annotation = annotations.get(name, _empty) - parameters.append(Parameter._from_valid_args(name, kind, defaults[offset], annotation)) + parameters.append(make_param(name, kind, defaults[offset], annotation)) if posonly_left: posonly_left -= 1 @@ -2382,7 +2393,7 @@ def _signature_from_function(cls, func, skip_bound_arg=True, if func_code.co_flags & CO_VARARGS: name = arg_names[pos_count + keyword_only_count] annotation = annotations.get(name, _empty) - parameters.append(Parameter._from_valid_args(name, _VAR_POSITIONAL, _empty, annotation)) + parameters.append(make_param(name, _VAR_POSITIONAL, _empty, annotation)) # Keyword-only parameters. for name in keyword_only: @@ -2391,7 +2402,7 @@ def _signature_from_function(cls, func, skip_bound_arg=True, default = kwdefaults.get(name, _empty) annotation = annotations.get(name, _empty) - parameters.append(Parameter._from_valid_args(name, _KEYWORD_ONLY, default, annotation)) + parameters.append(make_param(name, _KEYWORD_ONLY, default, annotation)) # **kwargs if func_code.co_flags & CO_VARKEYWORDS: index = pos_count + keyword_only_count @@ -2400,7 +2411,7 @@ def _signature_from_function(cls, func, skip_bound_arg=True, name = arg_names[index] annotation = annotations.get(name, _empty) - parameters.append(Parameter._from_valid_args(name, _VAR_KEYWORD, _empty, annotation)) + parameters.append(make_param(name, _VAR_KEYWORD, _empty, annotation)) # Is 'func' is a pure Python function - don't validate the # parameters list (for correct order and defaults), it should be OK.