skip to content

How does inspect.Signature.bind differ from bind_partial, and when do you use each?

level: middleimportance: should knowfreq 45%

answer

  1. One is strict, one is lenient
  2. About matching a call to parameters
  3. Missing required argument is the difference
  4. The result knows nothing about defaults yet
  5. bind, bind_partial, then apply_defaults

basics

~20 s

bind() maps a complete call onto the signature and raises TypeError if a required argument is missing; bind_partial() allows omissions. Both reject unknown names and surplus positionals, and neither fills in defaults until you call apply_defaults() on the result.

solid answer

~40 s

`sig.bind(*args, **kwargs)` matches a real call against the signature exactly as the interpreter would, raising `TypeError` with the same class of message — `missing a required argument: 'value'`, `too many positional arguments`, `got an unexpected keyword argument`. It returns an `inspect.BoundArguments` whose `.arguments` is an ordered mapping of **only what the caller actually supplied**, keyed by parameter name — so a decorator can read `bound.arguments["sensor_id"]` without caring whether that value arrived positionally or by keyword. `bind_partial` runs the same checks minus the missing-argument one, which is how you validate a half-built call the way `functools.partial` does. Neither fills defaults; `bound.apply_defaults()` mutates `.arguments` in place to add every default, an empty tuple for the `*args` parameter and an empty dict for `**kwargs`. `bound.args` and `bound.kwargs` rebuild the call, so `fn(*bound.args, **bound.kwargs)` re-invokes with normalised arguments.

code

python · 12 lines
python
import inspect

def store(sensor_id, value, *, unit="C"):
    return sensor_id, value, unit

sig = inspect.signature(store)
bound = sig.bind("edge-01", 21.5)
print(bound.arguments)
bound.apply_defaults()
print(bound.arguments)
print(store(*bound.args, **bound.kwargs))
print(sig.bind_partial(value=21.5).arguments)

go deeper

for a junior

Know that inspect can match a real call onto a function's parameters and hand you the values by name. Remember that bind is strict about missing required arguments and bind_partial is not.

for a middle

Explain BoundArguments: .arguments holds only what was supplied, apply_defaults fills the rest, and .args/.kwargs rebuild the call. Be able to write the decorator that reads a named argument regardless of how the caller passed it.

for a senior

Show judgement about where binding belongs in a request path — validating early with the interpreter's own error messages, normalising values by name, and avoiding the trap of mutating a shared default object that apply_defaults inserted.

for a principal

Decide whether call-shape validation is worth doing reflectively at all in a hot path, versus generating adapters once at registration time. Binding on every call buys uniform error messages and costs measurable time per invocation.

## The problem binding solves A decorator or middleware receives `(*args, **kwargs)` — the raw shape of the call, not its meaning. If a sensor-telemetry collector wants to tag every stored reading with its `sensor_id`, reading `args[0]` works right up until a caller writes `store(value=21.5, sensor_id="edge-01")`, and then it silently tags the wrong field. Binding is the fix: it maps the raw call onto the declared parameters, so you can look values up by **name** no matter how they were passed. ## Signature.bind `sig.bind(*args, **kwargs)` performs the same matching the interpreter performs at a real call — positional arguments fill positional slots left to right, keywords fill by name, surplus positionals go to `*args`, surplus keywords to `**kwargs` — and raises `TypeError` on every failure the interpreter would raise on: * `missing a required argument: 'value'` * `too many positional arguments` * `got an unexpected keyword argument 'oops'` * `multiple values for argument 'sensor_id'` What it does **not** do is check types, evaluate annotations, or call anything. It is a pure argument-matching exercise, which is why it is safe to run in a validation layer before you decide whether to invoke at all. ## BoundArguments The result is an `inspect.BoundArguments`. Its `.arguments` is an ordered mapping from parameter name to the value bound to it, containing **only the parameters that actually received a value**. That last point is the one candidates get wrong: after `sig.bind("edge-01", 21.5)` on `def store(sensor_id, value, *, unit="C")`, `.arguments` holds two keys, not three — `unit` is absent because the caller did not supply it. Two more attributes matter. `.args` and `.kwargs` reconstruct the positional tuple and keyword dict needed to make the call, so the canonical middleware shape is: ```python bound = sig.bind(*args, **kwargs) bound.arguments["sensor_id"] = normalise(bound.arguments["sensor_id"]) return fn(*bound.args, **bound.kwargs) ``` You mutate `.arguments` (it is a plain mutable mapping) and then let `.args`/`.kwargs` re-derive a valid call. The variadic parameters are handled for you: whatever sits under the `*args` name is spread back out, and the `**kwargs` dict is splatted. ## apply_defaults `bound.apply_defaults()` fills every parameter that was not supplied with its declared default, mutating `.arguments` in place. It also normalises the variadics: the `*args` entry becomes an empty tuple and the `**kwargs` entry an empty dict if they were absent. This is what you want when you are logging or hashing a call and need the *effective* arguments rather than the literal ones — two callers who differ only in whether they spelled out a default should produce the same record. The caveat is the classic mutable-default one: `apply_defaults()` inserts the *same* default object the function declared, so if that default is a list and you mutate `bound.arguments["tags"]`, you have mutated the function's default. Copy before mutating. ## bind_partial `sig.bind_partial(*args, **kwargs)` is the lenient sibling. It applies every check except "is anything missing", so it accepts `sig.bind_partial(value=21.5)` on a signature whose first parameter is required. It still rejects unknown keyword names and surplus positionals. Use it when the call is being assembled incrementally — a partial-application helper, a CLI that has parsed some flags but not others, a fixture builder validating what it has so far — and switch to `bind` at the moment you are about to invoke. `functools.partial` performs the same kind of partial matching, which is a useful mental anchor. ## Choosing between them Ask one question: *am I about to call this?* If yes, `bind` — you want the missing-argument error now, with a message identical to the one the interpreter would produce, rather than a confusing failure three frames deeper. If you are still collecting, `bind_partial`, then bind properly at the end. And in either case, reach for `apply_defaults()` only when you genuinely need the defaults materialised, because it changes what `.arguments` means. ## Cost, and where binding belongs Binding is not free: every call walks the parameters and builds a fresh mapping, then `.args`/`.kwargs` rebuild the call from it. That is fine at an edge — one bind per inbound request — and wasteful in an inner loop. Two habits keep it cheap. Compute the `Signature` **once**, when the decorator is applied or the handler registered, and close over it; calling `inspect.signature()` inside the wrapper repeats the expensive part on every invocation. And bind only when you actually need arguments by name: a decorator that merely times a call has no reason to bind at all, and should pass `*args, **kwargs` straight through.

  • What does BoundArguments.apply_defaults() do about the *args and **kwargs parameters?
    It normalises them: if the caller supplied no surplus positionals, the VAR_POSITIONAL entry becomes an empty tuple, and the VAR_KEYWORD entry becomes an empty dict. Together with the ordinary defaults that gives you a `.arguments` mapping with an entry for every declared parameter, which is what you want before logging or hashing the effective call.
  • After binding, how do you actually invoke the original callable?
    Call `fn(*bound.args, **bound.kwargs)`. `BoundArguments` derives those two from `.arguments`, spreading the VAR_POSITIONAL entry back into positionals and splatting the VAR_KEYWORD entry, so the reconstructed call is valid even after you have edited values in `.arguments`.
  • Does Signature.bind validate argument types or annotations?
    No. It only matches arguments to parameters — arity, names, duplicates and surplus — exactly as the interpreter does before the function body runs. Annotations are inert here; if you want type checking you have to read `.annotation` yourself and validate, or hand the bound values to a validation library.

bind is checking in with a completed form and being turned away for a blank mandatory field; bind_partial is having the half-filled form checked for typos before you finish it.

saying these in an interview costs you the question

  • Thinks bind() fills in default values automatically
  • Expects bind_partial to accept unknown keyword names
  • Reads args[0] in a decorator and assumes positional passing
  • Says bind raises ValueError for a missing argument
  • Believes bind type-checks arguments against annotations
  • Confuses BoundArguments.arguments with BoundArguments.kwargs

context