19.7 inspect.signature() and Introspecting Callables
The inspect.signature() function is the cornerstone of runtime callable introspection in Python, providing a powerful, standardized way to examine the parameters a function or method expects. It returns a Signature object, which is a rich, structured representation of the callable’s signature, far surpassing the basic information provided by older methods like inspect.getargspec() (now deprecated). This object allows you to programmatically understand not just the names of parameters, but also their kinds (positional, keyword, etc.), default values, and annotations, making it indispensable for building frameworks, decorators, validation libraries, and interactive tools.
The Signature and Parameter Objects
At its core, a Signature object contains an ordered mapping of parameter names to Parameter objects. Each Parameter object holds critical metadata about its corresponding argument.
import inspect
def example_func(a, b=10, *args, c, d=20, **kwargs):
pass
sig = inspect.signature(example_func)
print(sig) # Output: (a, b=10, *args, c, d=20, **kwargs)
for name, param in sig.parameters.items():
print(f"{name}: {param}")
# Output:
# a: a
# b: b=10
# args: *args
# c: c
# d: d=20
# kwargs: **kwargs
Each Parameter object has key attributes:
name: The name of the parameter as a string.default: The default value for the parameter. If the parameter has no default, this attribute is set toParameter.empty, a special sentinel value.annotation: The type annotation for the parameter, orParameter.emptyif none is provided.kind: A crucial attribute that describes how the parameter can be passed to the function. Its value is one of the constants from theinspectmodule.
Understanding Parameter Kinds
The kind attribute is essential for correctly interpreting a function’s signature, especially when advanced parameter types are involved. There are five possible kinds, which directly correspond to the parameter types discussed in this chapter:
POSITIONAL_OR_KEYWORD: A parameter that can be passed either by position or as a keyword argument. This is the default kind for most parameters (e.g.,aandbin the example above).VAR_POSITIONAL: A tuple of positional parameters, denoted by a single asterisk*(e.g.,*args).KEYWORD_ONLY: A parameter that must be passed as a keyword argument. These appear after aVAR_POSITIONALor a lone*in the function definition (e.g.,candd).VAR_KEYWORD: A dict of keyword parameters, denoted by two asterisks**(e.g.,**kwargs).POSITIONAL_ONLY: A parameter that can only be passed by position. This kind cannot be defined using the standard syntax in Python but is used by built-in functions (e.g.,divmod,range). You can create them using the/symbol in the parameter list.
def kind_demo(pos_or_kwd, /, standard, *, kwd_only, **kwargs):
pass
sig = inspect.signature(kind_demo)
for name, param in sig.parameters.items():
print(f"{name:12}: {inspect.Parameter.__getattribute__(param, 'kind')}")
# Output:
# pos_or_kwd : POSITIONAL_ONLY
# standard : POSITIONAL_OR_KEYWORD
# kwd_only : KEYWORD_ONLY
# kwargs : VAR_KEYWORD
Binding Arguments to Parameters
One of the most powerful features of the Signature object is the bind() method (and its partial variant, bind_partial()). These methods allow you to simulate a function call by mapping the provided arguments to the function’s parameters according to Python’s rules. This is incredibly useful for validating arguments before a call or for forwarding arguments from one function to another.
def complex_func(a, b, *args, option=True, **kwargs):
return a, b, args, option, kwargs
sig = inspect.signature(complex_func)
# Simulate a call: complex_func(1, 2, 3, 4, option=False, extra='value')
bound_args = sig.bind(1, 2, 3, 4, option=False, extra='value')
print("Bound arguments:", bound_args.arguments)
# Output: Bound arguments: {'a': 1, 'b': 2, 'args': (3, 4), 'option': False, 'kwargs': {'extra': 'value'}}
# You can now use this to call the function
print("Result:", complex_func(**bound_args.arguments))
# Output: Result: (1, 2, (3, 4), False, {'extra': 'value'})
If you provide invalid arguments (e.g., a missing required keyword-only parameter or an unexpected keyword argument), bind() will raise a TypeError, exactly as the real function call would. This allows for pre-call validation in decorators or frameworks.
Common Pitfalls and Best Practices
- Handling Built-ins and C Extensions: Many built-in functions and functions implemented in C (like those in many libraries) may not have metadata available for introspection. Calling
inspect.signature()on them might raise aValueError. Always wrap calls toinspect.signature()in a try-except block if you are unsure of the callable’s origin.try: sig = inspect.signature(len) except ValueError as e: print(f"Could not get signature: {e}") - The
Parameter.emptySentinel: Always check ifdefaultorannotationisParameter.emptyinstead ofNone. A parameter could have a default value ofNone(e.g.,param=None), which is semantically different from having no default at all. - Introspection Overhead: While
inspect.signature()is highly optimized, it is not free. Avoid calling it repeatedly inside tight loops. If you need to use a signature multiple times, cache the result. - Decorators Obfuscate Signatures: A common pitfall is that decorators, especially those that use
*argsand**kwargs, often hide the original signature of the wrapped function. Thefunctools.wrapsdecorator helps preserve metadata, but for perfect signature preservation, you may need to use theinspectmodule within your decorator or leverage third-party libraries likewrapt.