Argument Binding and Call Simulation¶
Page Maps¶
graph LR
family["Python Programming"]
program["Python Meta-Programming"]
section["Signatures Provenance Runtime Evidence"]
page["Argument Binding and Call Simulation"]
capstone["Capstone evidence"]
family --> program --> section --> page
page -.applies in.-> capstone
flowchart LR
orient["Orient on the page map"] --> read["Read the main claim and examples"]
read --> inspect["Inspect the related code, proof, or capstone surface"]
inspect --> verify["Run or review the verification path"]
verify --> apply["Apply the idea back to the module and capstone"]
Knowing a callable's signature is useful. Using that signature to simulate real call matching is where Module 03 starts paying off in wrappers, validators, RPC adapters, and tooling.
This page focuses on the binding helpers attached to Signature objects:
.bind().bind_partial()BoundArguments.apply_defaults()
Treat this as the page where signature evidence becomes operational. A learner who understands signatures but still hand-matches arguments has not yet internalized the main discipline of the module.
The sentence to keep¶
When a wrapper, validator, or adapter needs to understand a call, ask:
can I let
Signature.bind()do the interpreter-like matching instead of reimplementing it myself?
The answer should usually be yes.
Keep one second question beside it:
do I need full call validation, or am I deliberately working with a partial future call?
Why binding matters¶
Decorators and adapters often need to answer questions like:
- did the caller satisfy the required arguments?
- which value ended up bound to which parameter?
- did the caller try to pass a positional-only parameter by keyword?
- which defaults should be filled in before validation or logging?
Those are argument-matching questions, not merely signature-display questions.
bind() exists so tools can reuse Python's own call rules instead of inventing partial,
inconsistent copies of them.
That matters most in the places self-study learners often forget:
- positional-only parameters
- keyword-only parameters
- duplicate value assignment
- missing required arguments
- the difference between explicitly provided values and default-filled values
bind() versus bind_partial()¶
The two binding helpers answer related but different questions:
sig.bind(*args, **kwargs)requires all required arguments to be presentsig.bind_partial(*args, **kwargs)allows required arguments to remain unbound
That makes the split useful:
bind()is the right default for call validation and forwardingbind_partial()is useful for staged application, partial wrappers, or progressive configuration
Use this decision table when the choice feels fuzzy:
| If the tool needs to know... | Better helper | Why |
|---|---|---|
| "Would Python accept this call right now?" | bind() |
full callability is the question |
| "What has been supplied so far in a staged configuration?" | bind_partial() |
partial state is intentional |
| "Which defaults should be materialized before review or logging?" | bind() or bind_partial(), then apply_defaults() only if needed |
default filling is a separate decision from binding itself |
One picture of binding as call simulation¶
graph TD
callable["Callable contract"]
sig["Signature"]
incomingCall["Incoming args / kwargs"]
bound["BoundArguments<br/>parameter -> value mapping"]
defaults["apply_defaults()<br/>fills omitted defaulted parameters"]
callable --> sig
incomingCall --> bound
sig --> bound --> defaults
Caption: binding turns a call attempt into explicit parameter/value relationships using interpreter-like rules.
A basic example¶
import inspect
def demo(a, /, b, *, c=False, **kw):
pass
sig = inspect.signature(demo)
ba = sig.bind(10, 20, extra=1)
assert ba.arguments == {"a": 10, "b": 20, "kw": {"extra": 1}}
The important point is not the dictionary itself. It is that the mapping came from interpreter-aligned matching rules rather than from hand-written parsing logic.
That is the review win. The tool is not merely "using inspect." It is reusing the same
binding logic the runtime itself would apply at the call boundary.
Defaults are not applied automatically¶
Binding and default application are separate steps:
import inspect
def demo(a, /, b, *, c=False):
pass
sig = inspect.signature(demo)
ba = sig.bind(10, 20)
assert "c" not in ba.arguments
ba.apply_defaults()
assert ba.arguments["c"] is False
That separation matters because some tools want only explicitly supplied arguments, while others want a complete view including defaults before validation, caching, or logging.
Strong review language here sounds like:
- "we bind first to establish argument truth"
- "we apply defaults only because the next step needs a complete mapping"
Weak review language sounds like:
- "we fill defaults because it seemed easier"
- "we just turn everything into one dict immediately"
Binding failures are features, not inconveniences¶
If the call shape is invalid, bind() raises TypeError with interpreter-like messages.
import inspect
def demo(a, /, b):
pass
sig = inspect.signature(demo)
try:
sig.bind(a=10, b=20)
except TypeError as exc:
print("Expected:", exc)
That exception is useful evidence:
- the call shape is wrong
- Python would reject it too
- your wrapper does not need to invent its own slightly different rule
Strong wrapper code usually preserves or lightly adapts these failures rather than replacing them with vague custom messages.
If a wrapper changes the error text, it should still preserve the same contractual reason for failure. Otherwise the wrapper becomes harder to trust than the underlying callable.
Bound arguments are useful beyond validation¶
Once you have a BoundArguments object, you can use it for:
- validation
- tracing and logging
- standardized forwarding
- cache key construction
- documentation or error reporting
This is why binding belongs in Module 03 before decorators. It is one of the cleanest ways to keep later wrapper behavior honest.
Review checkpoint:
- binding tells you which parameter got which value
- it does not tell you whether the callable body will accept those values semantically
- later validation should start from the bound mapping, not replace it with looser custom reasoning
A small validation pattern¶
import inspect
def validate_call(func, *args, **kwargs):
sig = inspect.signature(func)
ba = sig.bind(*args, **kwargs)
ba.apply_defaults()
return ba.arguments
This is intentionally small, but the runtime contract is large:
- cache the signature in real code when repeated calls matter
- let binding establish argument truth
- run later validation against that established mapping
That ordering matters. If validation runs before binding, the validator is often forced to recreate the very call rules Python already knows.
Binding is stronger than manual tuple-and-dict reasoning¶
Many fragile wrappers do some version of:
- count positional arguments manually
- merge keyword arguments manually
- guess whether a name was required
- miss positional-only or keyword-only edges
That is exactly the kind of low-quality reimplementation Module 03 is meant to prevent.
If the runtime already has a precise call-matching model, use the model.
Common weak alternatives to reject explicitly:
- counting positional arguments manually
- merging keyword dictionaries manually
- special-casing "optional" parameters from docstrings or annotations
- assuming a wrapper with
*args, **kwargshas no recoverable contract
Guided lab: compare full, failed, and partial binding¶
Run:
$ python3 -m labs.runtime_evidence |
python3 -c 'import json, sys; print(json.load(sys.stdin)["binding"])'
{'bound_arguments': {'incident_id': 'INC-42', 'metadata': {'owner': 'platform'}, 'retries': 2, 'severity': 'warning'}, 'evidence_strength': 'strong-call-matching', 'keyword_for_positional_only_rejected': True, 'partial_arguments': {'retries': 3}}
The three outcomes answer different questions:
- full binding plus defaults produces the mapping an actual call would receive
- passing
incident_idby keyword is rejected because the signature marks it positional-only - partial binding accepts only
retriesbecause the tool is intentionally assembling an incomplete call
strong-call-matching means Python's argument rules accepted or rejected the shape. It
does not mean the eventual function body succeeds.
Failure route¶
Replace bind() with bind_partial() for the complete-call validation path. A call
missing incident_id will now appear acceptable even though actual invocation cannot
proceed. Record the different question partial binding answers, restore full binding, and
run make evidence-lab-test.
Transfer to the incident-plugin runtime¶
Run the capstone preflight:
$ make capstone-bind-action
{
"action_name": "deliver",
"bound_arguments": {
"severity": "warning",
"summary": "node-1 crossed 90%",
"title": "CPU high"
},
"executed": false,
"group": "delivery",
"plugin_name": "console"
}
This is not simulated application success. The public
bind_action_arguments(...) API retrieves the registered action's stored
inspect.Signature, calls bind(...), applies defaults, and returns the interpreter-
aligned argument mapping. It does not construct the plugin or invoke the action.
The capstone proof uses an event-recording plugin to assert that both side effects remain
absent. Invalid action shapes raise TypeError during preflight. Actual action wrappers
use the same binding rule before invocation and history recording, so preflight and
execution do not drift into separate hand-written argument models.
Review rules for binding logic¶
When reviewing call-validation or forwarding code, keep these questions close:
- is the code using
bind()or reimplementing argument matching by hand? - does the code distinguish full binding from partial binding?
- are defaults applied only when the downstream logic actually needs them?
- are
TypeErrorfailures from binding preserved clearly enough to remain useful? - is signature lookup cached in repeated wrapper paths where cost matters?
Self-study lab¶
Before leaving this page, write one example in this four-step format:
- the call attempt
- the exact binding helper you chose
- the resulting bound mapping or binding failure
- one sentence explaining why a hand-written matcher would be weaker here
If the fourth step feels optional, the main lesson has not landed yet.
What to practice from this page¶
Try these before moving on:
- Write a helper that binds a call, applies defaults, and returns the bound mapping.
- Compare
bind()withbind_partial()on the same signature and explain the difference. - Force one binding failure involving a positional-only or keyword-only parameter and explain why the failure is a feature.
If those feel ordinary, the next step is provenance: useful evidence about where code came from, with honest limits.
Continue through Module 03¶
- Previous: Signature Contracts and Parameter Kinds
- Next: Provenance Helpers and Best-Effort Recovery
- Practice: Exercises
- Terms: Glossary