Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Closures & Decorators  ›  Lesson

Callables, *args & Signatures

Callables and Signatures 22 minWhat can be called, and exactly how a call's arguments bind to parameters
You're building a piece ofQuarry — the middleware ring
This piece — call_stage(): Calls any stage by name with the arguments a config file supplies, and reports a bad call instead of crashing.
Scenario The operations team writes Quarry's pipelines in a config file, and a typo in step 9 must be caught before step 1 touches any data.
Your task
Write call_stage(name, args, kwargs). Look the stage up in the provided STAGES registry; an unknown name returns {"error": "unknown stage"}. Check the call with inspect.signature(stage).bind(*args, **kwargs) before making it; a binding failure returns {"error": "bad call"}. Otherwise call the stage and return {"ok": result}. Example: call_stage("scale", [4], {"factor": 2.5}) → {"ok": 10.0}.

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.

resample("cpu", 5, "tail", method="max", window=3) binding into series, rate, *extra, then a keyword-only wall, then method and **options — and sig.bind("cpu") alone raising TypeError because rate is missing

Build a stage registry from anything callable

python

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

python
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
What does this print?
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:

broken — fix itTypeError

Run 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:

What does this print?
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:

step through it
1import inspect
2 
3def resample(series, rate, *extra, method="mean", **options):
4 return None
5 
6sig = inspect.signature(resample)
7bound = sig.bind("cpu", 5, "tail", method="max", window=3)
8slots = dict(bound.arguments)
9 
10try:
11 sig.bind("cpu")
12 verdict = "ok"
13except TypeError:
14 verdict = "bad call: rate is missing"
15print(slots, verdict)

The order the binder works in:

Drawing diagram…

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:

python

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

What does this print?
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. ✅

Practice — level up

0/6 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upWhich of these can be called?

The provided THINGS dict maps names to objects. Write callable_names(names) returning, in the given order, only the names whose object is callable.

callables
DrillWhere did each argument land?

For the provided target(a, b=2, *rest, c=0, **extra), write bind_report(args, kwargs) that binds the call with inspect.signature(...).bind, applies defaults, and returns {"a", "b", "rest", "c", "extra"} — with rest as a list.

callables*argskeyword-only
BuildA callable that keeps score

Write a class Threshold(limit) whose instances are callable: calling one with a value returns value >= limit and counts how many values were over the limit in self.over. flag_values(limit, values) returns {"flags": [...], "over": n}.

callablesclasses
BossA decorator that checks calls first

Write a decorator checked(func): its wrapper binds the call against func's signature and returns the string "bad call" instead of raising if binding fails; otherwise it returns func's result. Keep the identity with @wraps. checked_calls(calls) runs each [args, kwargs] pair against the provided resample, and returns {"signature": str(inspect.signature(wrapped)), "results": [...]}.

callablesdecoratorswraps
CapstoneCheck the whole plan before running any of it

A plan is a list of {"stage", "args", "kwargs"} dicts. Write run_plan(plan): first check every step (unknown stage or a call that fails to bind), and if any are bad return {"ok": false, "bad_steps": [indexes], "results": []} without running anything. Otherwise run each step and return {"ok": true, "bad_steps": [], "results": [...]}. Use the provided STAGES.

callables*argskeyword-only
ExplainWhat *args, **kwargs buys a wrapper, and what it costs

Explain why decorator wrappers are written as def wrapper(*args, **kwargs) and call func(*args, **kwargs). Then explain what that costs anyone inspecting the decorated function, and how @wraps recovers it.

call_stage.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
call_stage(name, args, kwargs) → dictCall a registered stage by name with config-supplied arguments, reporting a bad call instead of crashing.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.