Inspect Function Signatures and Validate Arguments
Use inspect.signature and bind to validate a proposed function call without running the function.
Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons
Video lesson: Inspect Function Signatures and Validate Arguments
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
A plugin system or command dispatcher may receive a function from another part of a program. Before calling it, the dispatcher needs to know which arguments the function accepts and which ones are required. Looking at a function's internal variable names seems tempting, but local variables are mixed in with parameters. Python's `inspect.signature` exposes the callable's public parameter structure, including defaults and keyword-only parameters. It also has a `bind` method that checks whether supplied positional and keyword arguments fit that signature. Today we will reproduce a misleading introspection shortcut, inspect a real
- 00:40
signature, bind a valid call, and verify two error paths. These examples run entirely in memory. We are not building a generic dependency- injection framework; we are learning the precise difference between a function's parameters and arbitrary names used inside its body. The important question is what a caller may supply, not which temporary variables the implementation happens to create. Watch the terminal output at each stage, then try the practice cases in the browser console. The code object lists variable names used by the function, not only names that a caller may supply. `total` appears because it is created inside `calculate`, but
- 01:22
`calculate(total=12)` would not be a valid call. A dispatcher that treats every `co_varnames` entry as a parameter might demand a value for `total` or pass it by mistake. Code objects also expose lower-level details such as argument counts, but using them to reconstruct Python's full call rules becomes error-prone once keyword- only, positional-only, variable-length arguments, and defaults appear. `inspect.signature` already performs that interpretation for ordinary Python callables. The second output, twelve point zero, shows the function works with only `price` because `tax` has a default.
- 02:03
A simple list of names also does not tell us that distinction. The wrong shortcut is attractive because it seems compact; the bug becomes clear when we compare its output with what the function actually accepts. The signature prints the call shape we recognize from the function definition. `price` can be passed positionally or by keyword and has no default, so it is required. The star makes `tax` keyword-only, and its default means the caller can omit it. `total` is absent because it is not a parameter. The `Parameter.empty` sentinel distinguishes no default from a default value; do not use a truthiness check, because zero, False, and None can all be
- 02:43
valid defaults. Parameter kinds matter too. A generic adapter cannot safely assume every value may be supplied by keyword if a callable has positional-only parameters. `inspect.signature` can represent those rules so we do not need to inspect the code object and guess. Some built-in or extension callables may not expose a usable signature, so production tools should handle introspection failure. For our ordinary Python function, the result is clear and enough to validate an incoming call before executing the function body. `bind` checks that the supplied arguments fit the signature and returns a
- 03:24
`BoundArguments` object. We call `apply_defaults` so omitted optional parameters appear in the mapping. The first mapping contains price ten and tax one tenth, and invoking the function yields eleven. The second call supplies only price, so the mapping receives the default tax of one fifth. `BoundArguments` also provides `.args` and `.kwargs` for forwarding the validated call. Binding does not execute the function and does not validate domain rules such as requiring a nonnegative price. It only validates Python's call structure: required parameters, duplicates, unexpected names, and positional versus keyword restrictions.
- 04:08
That makes it useful at the edge of a dispatcher. Keep business validation in the callable or an explicit validator. Also avoid using `bind_partial` when you require a complete call; that method intentionally permits missing required arguments because it is meant for partial application. Here, `bind` expresses our goal better. A valid binding can be forwarded to the function and produces twelve. The first invalid case supplies no price, so binding raises `TypeError` before the function body runs. The second supplies a `discount` keyword that the function does not accept, producing another `TypeError`.
- 04:48
We print the exception type rather than depending on the exact wording of an error message across Python versions. The final success message appears only when both errors were detected. Try binding `calculate(10, 0.1)` in the lesson console; it should fail because `tax` is keyword-only. Then try the same value as `tax=0.1`, which should succeed. This is a practical way to learn what a signature communicates. The static shape of a call can be checked before execution, but the function's result still needs ordinary tests. Use `inspect.signature` when you genuinely need to adapt to another callable; for
- 05:29
code you control directly, a normal explicit function call is often simpler.
Understand the concept
A configurable callback may receive arguments assembled from a form or a file. Passing an unexpected keyword or omitting a required one produces TypeError, sometimes deep inside an application. inspect.signature exposes the callable's declared parameters, while Signature.bind checks whether supplied arguments fit. Binding answers whether a call shape is valid; it does not execute the function.
Consider send_notice(recipient, message, *, urgent=False). A mapping containing recipient and message binds successfully. A mapping with reciever instead of recipient fails because that keyword is not declared and recipient remains missing. Binding also respects positional-only, keyword-only, default, and variadic parameters according to the function signature. The resulting BoundArguments object exposes the normalized association between names and supplied values.
A successful bind is only a shape check. It does not prove that recipient contains a real address, that urgent is a Boolean, or that the function body cannot raise. Runtime type annotations are not enforced automatically either. After binding, an application still needs domain validation and an intentional call. This exercise focuses on the boundary before execution, where errors can be reported clearly.
The checked validate_kwargs function accepts a callable and a mapping of proposed keyword arguments. Reject a noncallable or nonmapping input with TypeError; use signature.bind(**kwargs) and return its arguments as an ordinary dictionary. Tests cover a valid call, a missing required parameter, an unexpected keyword, and a callback that must not be run during validation. This is useful for plugin configuration, CLI dispatch, and other controlled callback systems.
- Signature describes parameters
- bind validates call shape without execution
- BoundArguments maps names to supplied values
- Binding does not validate semantics or annotations
See it step by step
Read the code, predict the output, then compare it with the result.
01. Inspect keyword-only and default parameters
import inspect
def send_notice(recipient, message, *, urgent=False): pass
print(inspect.signature(send_notice))(recipient, message, *, urgent=False)The star marks urgent as keyword-only; its default does not need to be supplied.
02. Bind without calling
import inspect
def send_notice(recipient, message, *, urgent=False):
print('SENT')
bound = inspect.signature(send_notice).bind(recipient='team', message='Ready')
print(dict(bound.arguments)){'recipient': 'team', 'message': 'Ready'}The function body never runs; bind only maps and validates parameters.
03. Expose a misspelled keyword
import inspect
def send_notice(recipient, message): pass
try:
inspect.signature(send_notice).bind(recipient='team', reciever='typo', message='Ready')
except TypeError as error:
print(type(error).__name__)
print('reciever' in str(error))TypeError
TrueThe binding error identifies the unexpected keyword before any callback side effect.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Read a callable's declared shape
The first example prints a signature with two required parameters and one keyword-only parameter. The star means urgent cannot be passed positionally; its default False means a caller may omit it. Reading this representation before wiring a callback makes the proposed call shape visible without guessing from a later TypeError.
A signature describes parameter names, order, and defaults. It does not inspect what the function body will do, and annotations do not automatically enforce value types. Use this tool to validate how arguments would be supplied. Domain rules, such as whether recipient is a real address, still need separate checks.
import inspect
def send_notice(recipient, message, *, urgent=False): pass
print(inspect.signature(send_notice))(recipient, message, *, urgent=False)- Print inspect.signature for a small function.
- Identify required and keyword-only parameters.
- Keep shape validation separate from value validation.
Bind arguments without running side effects
The second function would print SENT if it ran. inspect.signature(...).bind maps recipient and message into a BoundArguments object, then the example prints that mapping. SENT does not appear. This is precisely the distinction needed when arguments are assembled from configuration and must be checked before invoking a side-effecting callback.
A successful bind says the names and count fit the declared signature. It does not prove the message is useful or the callback will succeed. The checked function should return a plain dictionary of bound arguments and must never call the candidate. Test that guarantee with a function whose body raises or records a side effect if reached.
import inspect
def send_notice(recipient, message, *, urgent=False):
print('SENT')
bound = inspect.signature(send_notice).bind(recipient='team', message='Ready')
print(dict(bound.arguments)){'recipient': 'team', 'message': 'Ready'}- Obtain the callable's signature.
- Bind the proposed keyword mapping.
- Confirm the function body did not execute.
Report a misspelled keyword early
The last run supplies reciever rather than recipient. bind raises TypeError before any function body runs, and the diagnostic contains the misspelled name. This gives a dispatcher a chance to reject bad configuration at its boundary instead of failing after earlier work has already happened.
Validate that the supplied object is callable and that the proposed values form a mapping before unpacking them with double stars. Then let bind enforce positional-only, keyword-only, and required-parameter rules. Avoid swallowing its error and returning a misleading empty mapping; the caller needs to know why the call shape failed.
import inspect
def send_notice(recipient, message): pass
try:
inspect.signature(send_notice).bind(recipient='team', reciever='typo', message='Ready')
except TypeError as error:
print(type(error).__name__)
print('reciever' in str(error))TypeError
True- Try the unexpected keyword deliberately.
- Observe the TypeError before invocation.
- Propagate a clear validation error to the caller.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
Check callable and isinstance(kwargs, collections.abc.Mapping); call inspect.signature(callback).bind(**kwargs) and return dict(bound.arguments).
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Executing while trying to validate
callback(**kwargs) # validation onlyinspect.signature(callback).bind(**kwargs)What happens: A notification is sent before the proposed call is approved.
Bind the argument shape without executing side effects.
Treating annotations as runtime guards
signature.bind(**kwargs) # all values are now safebound = signature.bind(**kwargs)
# Validate domain values separately.What happens: A wrongly typed or invalid address can still pass.
Signature binding does not check value semantics or type hints.