Using inspect.signature, how do you tell whether a callable accepts a given keyword argument?
answer
- Two questions, in a fixed order
- The catch-all short-circuits everything
- Presence in the mapping is not enough
- A name you cannot say as a keyword
- Decide at registration, not per call
basics
~10 sWalk inspect.signature(func).parameters: if any parameter has kind VAR_KEYWORD the callable accepts any keyword; otherwise the name must be present with kind POSITIONAL_OR_KEYWORD or KEYWORD_ONLY. POSITIONAL_ONLY does not count.
solid answer
~40 sTake `inspect.signature(func).parameters` and ask two questions. First, does any parameter have `kind` `VAR_KEYWORD`? If so the callable swallows any keyword and the answer is yes. Otherwise look the name up in the mapping: it must exist **and** its kind must be `POSITIONAL_OR_KEYWORD` or `KEYWORD_ONLY`. A `POSITIONAL_ONLY` parameter with the same name does not count — its name is not part of the calling convention. Wrap the whole thing in `try/except (ValueError, TypeError)`, since some C callables carry no introspection data and non-callables raise. An alternative that outsources the rules entirely is `sig.bind_partial(**{name: value})` inside a `try/except TypeError`. In production, resolve this once when a callable is registered and cache the verdict rather than sniffing on every call.
code
python · 19 linesimport inspect
from inspect import Parameter
def accepts_keyword(func, name):
try:
params = inspect.signature(func).parameters
except (ValueError, TypeError):
return False
if any(p.kind is Parameter.VAR_KEYWORD for p in params.values()):
return True
p = params.get(name)
return p is not None and p.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY)
def old_hook(invoice): ...
def new_hook(invoice, *, context=None): ...
def strict_hook(invoice, context, /): ...
for hook in (old_hook, new_hook, strict_hook):
print(hook.__name__, accepts_keyword(hook, "context"))go deeper
Recall that a callable's parameters can be listed at runtime and that each one records how it may be passed. You are not expected to write the capability check yet, only to know the mapping exists.
Be able to code the two-step check: look for a var-keyword catch-all first, then look the name up and verify its kind. Explain why membership alone gives the wrong answer for a positional-only parameter.
Show production instincts — handle the exceptions inspect.signature can raise, resolve the verdict once at registration so failures surface at startup, and explain why catching TypeError from the call itself conflates two different faults.
Take a position on capability sniffing as a design: fine for adapting to callables you do not own, a smell when applied to an interface you control, where a mandated keyword-collecting parameter or a versioned context object removes the guessing entirely.
### The question behind the question Say an invoice-PDF renderer loads hook callables from eleven teams. Newer hooks want a `context=` keyword carrying the render context; older ones were written before it existed. The host has to decide, per hook, whether passing `context=` is legal — because guessing wrong raises `TypeError` mid-render, after part of the document has been written and has to be rolled back. The naive answers are all wrong in the same way: reading the source, scraping the raw code object, or catching `TypeError` from the call itself. The last one is the subtlest trap — a `TypeError` raised *inside* the hook is indistinguishable from a `TypeError` raised by the *call*, so a broken hook silently gets classified as "does not accept context". ### The correct test `inspect.signature(func).parameters` answers it directly, in two steps: ```python import inspect from inspect import Parameter def accepts_keyword(func, name): try: params = inspect.signature(func).parameters except (ValueError, TypeError): return False if any(p.kind is Parameter.VAR_KEYWORD for p in params.values()): return True p = params.get(name) return p is not None and p.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY) ``` **Step one: the catch-all.** A `VAR_KEYWORD` parameter — the `**`-collecting one — accepts every keyword name that is not already bound to a declared parameter. If one is present, the answer is yes without looking at the name at all. **Step two: the name and its kind.** Presence in the mapping is not enough. A parameter whose kind is `POSITIONAL_ONLY` can only be supplied by position; its name exists in the signature for documentation, not for calling. `def hook(invoice, context, /)` has a parameter literally called `context` and yet `hook(inv, context=ctx)` raises `TypeError`. Checking membership without checking `.kind` is the classic bug here, and it is exactly the case that only bites once a library adopts positional-only parameters. ### Why the exception handling is not optional `inspect.signature()` raises `ValueError` for a C callable with no introspection data and `TypeError` for something that is not callable. A plugin host takes callables it did not write, so both are reachable. Returning a conservative `False` — "assume it does not accept the keyword" — degrades to the older calling convention rather than crashing the render. ### The alternative: a dry-run bind Instead of encoding the rules yourself, you can let the interpreter's rules decide: ```python try: sig.bind_partial(**{name: value}) except TypeError: accepted = False ``` `bind_partial()` does not require a complete argument list, so this tests the one keyword in isolation, and it inherits every rule — positional-only, keyword-only, duplicates — for free. It is slower than the kind check and allocates, so it suits a registration-time decision more than a hot path. ### The senior judgement Three points separate a working answer from a good one. **Decide once, at registration.** Building a `Signature` is the expensive part of this. With eleven hooks, introspect each one when it is registered, store a small record — "takes context: yes/no" — and consult that record per render. A failure then surfaces at startup, when nothing has been written yet, instead of halfway through a document. **Know that signatures can lie.** A callable can declare `__signature__`; a thin forwarding layer that does not preserve the wrapped signature reports `(*args, **kwargs)`, which reads as "accepts every keyword" and is technically true of the layer but may not be true of what it forwards to. Introspection tells you the convention the object advertises, not a guarantee about the body. **Prefer a contract to sniffing.** The best answer usually ends by questioning the premise: a hook protocol that mandates `**kwargs`, or a versioned hook interface where new arguments arrive in an explicit context object, removes the need to introspect at all. Runtime capability detection is the right tool for adapting to callables you do not control; it is a poor substitute for an interface you do control. ### Related mistakes worth naming Testing `name in sig.parameters` alone; assuming `**kwargs` in the *wrapper* means the *target* accepts the keyword; introspecting per call in a loop; and treating a `TypeError` escaping the call as proof of an argument mismatch.
- A hook declares a parameter named `context` yet passing context= raises TypeError. Why?Its kind is `POSITIONAL_ONLY` — declared before a `/` marker. For such a parameter the name is documentation, not part of the calling convention, so it can only be supplied by position. If the same callable also has a `**`-collecting parameter, the keyword is not rejected but lands in that mapping instead of binding to the parameter you meant, which is worse: it fails silently.
- Why is catching TypeError from the actual call a poor substitute for this check?Because a `TypeError` raised inside the callable's body looks identical to one raised by argument matching, so a genuinely broken hook is misdiagnosed as "does not accept the keyword" and quietly downgraded. A dry-run `Signature.bind_partial()` inside `try/except TypeError` gives the same verdict without executing anything, which keeps the two failure classes apart.
- Why resolve this at registration time rather than per call?Building a `Signature` is the expensive step — it inspects the code object, defaults and annotations — and the answer cannot change for a given callable. Introspect when the callable is registered, store the verdict, and read it per call. It also moves the failure earlier: a hook that cannot be introspected is reported at startup rather than partway through a job whose partial output then has to be unwound.
saying these in an interview costs you the question
- Tests only whether the name is in sig.parameters
- Reads the raw code object instead of the Signature
- Forgets a var-keyword parameter accepts any keyword
- Assumes inspect.signature never raises for a C callable
- Introspects on every call instead of at registration
- Treats TypeError escaping the call as an argument mismatch