Callables, *args & Signatures
In short: callable means "you can put parentheses after it" — functions
and lambdas, yes, but also classes (calling one builds an instance), bound
methods, partial objects, and any instance whose class defines __call__;
callable(x) asks directly. A function signature declares up to five kinds of
parameter in a fixed order — positional-only (before /), positional-or-keyword,
*rest (extra positionals), keyword-only (after */*rest), **extra (extra
keywords). Python binds positionals left-to-right first, then keywords by name,
then fills defaults — a slot filled twice or left required-and-empty is a
TypeError. That's exactly why every decorator wrapper is def wrapper(*args, **kwargs): it accepts anything and forwards it unchanged — and
inspect.signature(fn).bind(...) lets you check whether a call would succeed
without making it, which is how a config-driven dispatcher validates before
running anything.

Build a stage registry from anything callable
For a stage registry, this is good news: it can hold any callable — a plain
function, a partial with settings pre-filled, or a Threshold(100) instance
carrying its own configuration. The caller just calls it.
Read a stage's full parameter list
| Kind | Declared as | Filled by |
|---|---|---|
| positional-only | before / |
position only |
| positional-or-keyword | ordinary names | position or name |
| var-positional | *rest |
leftover positionals, as a tuple |
| keyword-only | after * or *rest |
name only |
| var-keyword | **extra |
leftover keywords, as a dict |
def f(a, b=2, *rest, c, **extra):
print(a, b, rest, c, extra)
f(1, 3, 5, 7, c=9, d=0)Stop low and high being swapped
Everything after *rest, or after a bare *, can only be passed by name —
useful the moment two arguments are easy to swap by accident:
TypeErrorRun it and read the message about positional arguments. Fix the call, not the function — the wall is there on purpose.
Don't remove the *. The wall makes clip(150, high=100, low=0) and
clip(150, low=0, high=100) mean the same thing, and makes the swapped
clip(150, 100, 0) impossible to write at all.
The opposite wall, /, makes a name private — the caller can never use it
by keyword, so it can't clash with a **kwargs key of the same name:
def label(name, /, **tags):
return f"{name} {tags}"
print(label("disk", name="dup"))Watching one call bind, slot by slot
inspect.signature gives you Python's own binding logic as a tool. bind(...)
does exactly what a real call would do to fill the slots, without calling the
function, and raises the same TypeError if the call is bad. Step through it:
The order the binder works in:
One-liner: why every wrapper says *args, **kwargs
In a definition, def wrapper(*args, **kwargs) means "accept anything."
In a call, func(*args, **kwargs) spreads them back out so the original
binds them exactly as if the caller had called it directly — the wrapper never
binds anything itself, it just passes the call along. The cost: the wrapper's
own signature says nothing useful, unless @wraps stored the original as
__wrapped__ for inspect.signature to follow:
Tools that read signatures — including a stage dispatcher — see straight through a well-behaved wrapper, and are blind to one that isn't.
Quick practice
import inspect
def resample(series, rate, *extra, method="mean", **options):
return None
sig = inspect.signature(resample)
try:
sig.bind("cpu")
verdict = "ok"
except TypeError:
verdict = "bad call"
print(verdict)Closure, class, or partial? Use partial when you're only pre-filling an
existing function's arguments. Use a closure for a small amount of private
state and one operation. Use a class with __call__ when the configuration
should be visible (over_100.limit), there's more than one operation, or you
want a readable repr in logs. A dispatcher that just calls things doesn't
care which one it gets — that's the whole point of "callable."
🎯 Your turn
Write call_stage(name, args, kwargs). Look the stage up in the provided
STAGES registry. If the name is unknown, return {"error": "unknown stage"}.
Otherwise, use inspect.signature(stage).bind to check the call before
making it — if binding fails, return {"error": "bad call"}; if it succeeds,
call the stage and return {"ok": result}.
call_stage("scale", [4], {"factor": 2.5})→{"ok": 10.0}call_stage("clip", [150, 0, 100], {})→{"error": "bad call"}call_stage("nope", [], {})→{"error": "unknown stage"}
Hint: bind first, call second. Wrapping the call itself in
try/except TypeError would also catch a TypeError raised by a bug inside
the stage, and misreport it as the config's fault.
Press ▶ Run, then ✓ Check. ✅
