Python Basics: Check Call Shapes with inspect.signature
A command adapter often receives positional arguments and named options before it calls a trusted function. inspect.signature can show the accepted shape and bind proposed arguments without running that function. Use it to produce clearer errors at an integration boundary, not to validate business data.
Prepare a scratch directory
These standalone examples were tested on Linux with Python 3.12.14. Use Python 3.8 or newer because the examples include positional-only parameters. Use Bash, a text editor, and the standard mktemp, rm, and rmdir utilities. No third-party packages are required. Run without -O so the example assertions remain enabled.
Create a fresh directory and keep using the same terminal:
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1Save each named file below as UTF-8. Do not name your script after a standard-library module.
1. Distinguish parameter kinds
Save as describe_report_signature.py:
import inspect
def render_report(destination, /, title="Summary", *, width=72):
return destination, title, width
signature = inspect.signature(render_report)
print("Signature:", signature)
for name, parameter in signature.parameters.items():
print(f"{name}: {parameter.kind.name}")
assert signature.parameters["width"].kind is inspect.Parameter.KEYWORD_ONLYRun it:
python3 -B describe_report_signature.pyOutput:
Signature: (destination, /, title='Summary', *, width=72)
destination: POSITIONAL_ONLY
title: POSITIONAL_OR_KEYWORD
width: KEYWORD_ONLYThe slash makes destination positional-only. The star makes width keyword-only. The signature preserves these distinctions, unlike a plain list of parameter names. Defaults are visible too, but an annotation or default does not enforce a runtime type check.
2. Bind values and fill defaults before calling
Save as bind_report_arguments.py:
import inspect
calls = []
def render_report(destination, /, *, width=72):
calls.append(destination)
return f"{destination}: width={width}"
signature = inspect.signature(render_report)
bound = signature.bind("preview.txt")
bound.apply_defaults()
assert calls == []
print("Bound:", dict(bound.arguments))
print("Calls before invocation:", len(calls))
print(render_report(*bound.args, **bound.kwargs))
assert calls == ["preview.txt"]Run it:
python3 -B bind_report_arguments.pyOutput:
Bound: {'destination': 'preview.txt', 'width': 72}
Calls before invocation: 0
preview.txt: width=72bind checks required arguments and their positions without invoking render_report. apply_defaults makes omitted defaults explicit. BoundArguments exposes args and kwargs in a form that can be passed back to the function. Inspecting a signature is not a promise that a later call will succeed: the function can still reject values or raise during its work.
3. Catch missing and unexpected options
Save as reject_invalid_call_shapes.py:
import inspect
def format_label(name, *, uppercase=False):
return name.upper() if uppercase else name
signature = inspect.signature(format_label)
cases = [
("missing", (), {}),
("unknown", ("draft",), {"color": "blue"}),
("valid", ("draft",), {"uppercase": True}),
]
for label, arguments, options in cases:
try:
bound = signature.bind(*arguments, **options)
except TypeError:
print(f"{label}: rejected")
else:
print(f"{label}: {format_label(*bound.args, **bound.kwargs)}")
partial = signature.bind_partial()
assert partial.arguments == {}
print("Partial binding allows missing name:", True)Run it:
python3 -B reject_invalid_call_shapes.pyOutput:
missing: rejected
unknown: rejected
valid: DRAFT
Partial binding allows missing name: Truebind_partial intentionally permits missing required arguments, which is useful when assembling a call in stages. Use bind when the call should already be complete. Catching a binding error separately avoids accidentally labeling a TypeError raised inside the called function as a bad argument list.
Apply this pattern to trusted callables with normal signatures. Some builtins cannot provide introspection metadata, and signature can raise ValueError or TypeError. A custom signature or decorator can also affect the reported interface. Do not use successful binding as a permission check, schema validator, or guarantee that executing an unknown callable is safe.
Clean up
In the same terminal, remove only the named sources, return to the original directory, and remove the empty scratch directory:
rm -- describe_report_signature.py bind_report_arguments.py reject_invalid_call_shapes.py
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirIf rmdir reports remaining files, inspect them instead of deleting unknown contents.
References
Acknowledgements
This post was created with assistance from AI.