Functions, methods, classes, and callable objects expose a contract: positional parameters, keyword arguments, defaults, annotations, and return information. The inspect module makes that contract available at runtime through inspect.signature(). Web frameworks, dependency-injection containers, command-line generators, validators, decorators, documentation tools, and dynamic adapters all rely on this API.
This guide explains how to obtain signatures, interpret parameter kinds, bind arguments, apply defaults, preserve metadata through decorators, create custom signatures, and recognize the limits of runtime introspection.
Your first Signature object
from inspect import signature
def create_user(name: str, age: int = 18) -> dict[str, object]:
return {"name": name, "age": age}
sig = signature(create_user)
print(sig)
The result is not merely formatted text. A Signature object contains ordered Parameter objects, a return annotation, and methods that validate the shape of a call.
Walking through parameters
for name, parameter in sig.parameters.items():
print(name)
print(parameter.kind)
print(parameter.default)
print(parameter.annotation)
parameters is an ordered mapping. Order matters because it reproduces the original declaration and Python’s real call rules.
The five Parameter kinds
Every parameter has a kind:
POSITIONAL_ONLY: may only be passed by position.POSITIONAL_OR_KEYWORD: accepts position or name.VAR_POSITIONAL: represents*args.KEYWORD_ONLY: appears after*and requires a name.VAR_KEYWORD: represents**kwargs.
def example(a, /, b, *args, c, **kwargs):
...
for p in signature(example).parameters.values():
print(p.name, p.kind)
Frameworks that generate forms, routes, or commands must respect these categories. Turning every input into a keyword argument breaks positional-only parameters.
Missing defaults and annotations
When a parameter has no default or annotation, the API uses inspect.Parameter.empty.
from inspect import Parameter
for p in sig.parameters.values():
if p.default is Parameter.empty:
print(p.name, "is required")
Do not compare the default with None, because None may be a legitimate default value.
Return annotations
from inspect import Signature
if sig.return_annotation is not Signature.empty:
print(sig.return_annotation)
A return annotation may be a class, generic expression, string, or another object. signature() does not replace typing.get_type_hints() when forward references must be resolved.
Validating calls with bind
bound = sig.bind("Ana", age=30)
print(bound.arguments)
bind() applies the same argument-assignment rules as the function call and raises TypeError for missing, duplicate, or unexpected arguments. It is valuable in adapters, dispatch systems, and wrappers.
Partial binding
partial = sig.bind_partial(name="Ana")
bind_partial() allows required arguments to remain absent. It is appropriate for functools.partial, builders, and configuration assembled in stages. Do not use it when a final call must already be complete.
Applying defaults
bound = sig.bind("Ana")
bound.apply_defaults()
print(bound.arguments)
Before apply_defaults(), the mapping contains only values supplied by the caller. Afterward, optional parameters receive their defaults, *args becomes an empty tuple, and **kwargs becomes an empty dictionary.
Working with BoundArguments
The object returned by binding exposes args, kwargs, and arguments. A normalized call can be forwarded directly:
result = create_user(*bound.args, **bound.kwargs)
Changing the ordered arguments mapping affects the derived properties. Do this carefully and validate types separately, because binding checks call structure rather than semantic correctness.
Methods and self
class Service:
def run(self, task: str) -> None:
...
print(signature(Service.run))
print(signature(Service().run))
The unbound method signature includes self; the bound method usually does not. A framework must decide whether it is inspecting a class attribute or a callable obtained from an instance.
Callable instances
class Converter:
def __call__(self, value: str, *, strict: bool = False) -> int:
return int(value)
print(signature(Converter()))
signature() can inspect objects implementing __call__. This makes callable instances useful in pipelines, configurable dependencies, and strategy objects.
Classes and constructors
class User:
def __init__(self, name: str, active: bool = True):
...
print(signature(User))
For classes, the visible call contract may come from metaclass __call__, __new__, or __init__. Custom metaclasses can therefore change what introspection reports.
Decorators can hide signatures
def log_calls(function):
def wrapper(*args, **kwargs):
print("call")
return function(*args, **kwargs)
return wrapper
Without additional metadata, the visible signature becomes (*args, **kwargs). Use functools.wraps to set __wrapped__ and preserve the original callable:
from functools import wraps
def log_calls(function):
@wraps(function)
def wrapper(*args, **kwargs):
return function(*args, **kwargs)
return wrapper
By default, signature() follows the __wrapped__ chain. The guide to Python decorators explains this pattern in depth.
Disabling wrapped traversal
signature(decorated_function, follow_wrapped=False)
Use follow_wrapped=False when you need to inspect the wrapper’s real runtime interface rather than the callable it wraps.
Custom signatures
Objects may expose __signature__. Frameworks use it to present a dynamically generated public interface. This changes introspection, not necessarily runtime behavior. A misleading custom signature can tell tools that a call is valid even when the wrapper rejects it.
Creating Parameter objects
from inspect import Parameter, Signature
parameter = Parameter(
"limit",
kind=Parameter.KEYWORD_ONLY,
default=100,
annotation=int,
)
new_signature = Signature([parameter], return_annotation=list)
Signature and Parameter objects are immutable. Build a new object or use replace() instead of mutating existing instances.
Replacing parts safely
updated = sig.replace(return_annotation=dict[str, object])
Parameter.replace() can change a name, default, kind, or annotation. The resulting sequence must still follow Python’s ordering rules: positional groups first, variadic parameters in valid positions, and required parameters before optional ones within a category.
String annotations
With postponed annotations, a signature may contain strings rather than resolved objects. Modern Python versions expose options for annotation evaluation and formatting, but portable framework code should separate structural inspection from type resolution.
from typing import get_type_hints
hints = get_type_hints(create_user, include_extras=True)
The guide to Python get_type_hints covers namespaces, forward references, and security.
Built-ins and extension functions
Many C-implemented functions provide signature metadata, but not all callables are introspectable. signature() may raise ValueError when no signature can be provided and TypeError when the object is not a supported callable.
try:
sig = signature(obj)
except (TypeError, ValueError):
sig = None
Dependency injection
A dependency container can inspect parameters, resolve each annotated dependency, and invoke the function with constructed values. However, annotations are not runtime validation. The container still needs policies for defaults, aliases, variadic parameters, scopes, and useful error messages.
Generating a CLI
Required parameters may become positional command-line arguments, keyword-only parameters may become options, and booleans may become flags. The signature alone does not provide friendly help text, constraints, examples, or environment-variable mapping. Annotated metadata can supplement that information.
Caching signatures
Repeated introspection on hot paths can add overhead. Since callable signatures are usually stable, frameworks often cache them by object identity. Invalidate the cache if decorators or plugin systems modify __signature__ dynamically.
Security and privacy
Structural inspection does not execute the callable, but resolving annotations can evaluate names. Do not resolve hints from untrusted plugin code without isolation. Also avoid exposing default values in generated documentation or logs when those defaults may contain tokens, paths, or credentials.
Common mistakes
- Comparing defaults with None: use
Parameter.empty. - Ignoring parameter kinds: positional-only and keyword-only rules are real.
- Treating bind as type validation: it checks only call shape.
- Losing metadata in decorators: apply
functools.wraps. - Assuming every built-in is supported: catch TypeError and ValueError.
- Publishing a fake __signature__: introspection and runtime behavior may diverge.
Complete example: configurable executor
from inspect import Parameter, signature
from typing import get_type_hints
def execute(function, values: dict[str, object]):
sig = signature(function)
hints = get_type_hints(function, include_extras=True)
kwargs = {}
for name, parameter in sig.parameters.items():
if parameter.kind in {
Parameter.VAR_POSITIONAL,
Parameter.VAR_KEYWORD,
}:
continue
if name in values:
kwargs[name] = values[name]
elif parameter.default is Parameter.empty:
raise ValueError(f"missing value: {name}")
bound = sig.bind(**kwargs)
bound.apply_defaults()
return function(*bound.args, **bound.kwargs)
The example normalizes the call and detects missing values. A production executor must also validate types, handle positional-only parameters, support conversion, and produce domain-specific diagnostics.
Conclusion
inspect.signature() turns a callable contract into structured, reliable objects. With Signature, Parameter, and BoundArguments, you can validate call shape, generate interfaces, adapt wrappers, and preserve decorator metadata without manually parsing source code.
The official inspect.signature documentation defines the full API. Combine it with get_type_hints(), respect every parameter kind, and keep introspection separate from type validation and execution.







