What does inspect.signature() return, and how do you read a function's parameters from it?
answer
- Ask a callable how it may be called
- One object, then a mapping of parameters
- Each entry knows its kind and default
- Missing default is a sentinel, not None
- Five kinds span the calling grammar
basics
~10 sinspect.signature() returns a Signature object. Its .parameters attribute is an ordered mapping from parameter name to an inspect.Parameter, and each Parameter carries .kind, .default and .annotation. Printing the Signature renders the call form.
solid answer
~40 s`inspect.signature(func)` builds an `inspect.Signature` describing how that callable may be called. Its `.parameters` attribute is a read-only ordered mapping (a `mappingproxy`) from name to `inspect.Parameter`, and each `Parameter` exposes `.name`, `.kind`, `.default` and `.annotation`. A missing default or annotation is the `inspect.Parameter.empty` sentinel rather than `None`, because `None` is itself a legal default. `.kind` is one of five values — `POSITIONAL_ONLY`, `POSITIONAL_OR_KEYWORD`, `VAR_POSITIONAL`, `KEYWORD_ONLY`, `VAR_KEYWORD` — so the mapping tells you not only the names but how each one may be supplied. `str(sig)` renders the source-like form. It works on functions, bound methods (with the already-bound first parameter dropped), classes, `functools.partial` objects and most C builtins; a C callable with no introspection data raises `ValueError`.
code
python · 10 linesimport inspect
def render(path, /, pages=1, *, scale=1.0, **opts):
...
sig = inspect.signature(render)
print(sig)
for name, p in sig.parameters.items():
default = "-" if p.default is inspect.Parameter.empty else p.default
print(name, p.kind.name, default)go deeper
Be ready to say that inspect.signature() hands back a Signature whose .parameters maps each name to a Parameter with a kind and a default, and to print one in the REPL without hesitating.
Explain the five parameter kinds and why a missing default is inspect.Parameter.empty rather than None. An interviewer expects you to walk the mapping and branch on .kind rather than on the name.
Show that you know where signature() fails or surprises: ValueError on some C callables, the bound first parameter dropped for methods, and the real cost of building a Signature per call instead of caching it at registration.
Own the argument for making runtime introspection an explicit, one-time step in a plugin or dispatch design rather than an implicit cost sprinkled through hot paths, and for treating the reported convention as an API contract you version.
### The object you get back `inspect.signature(callable)` asks a callable to describe its own calling convention and returns an `inspect.Signature`. The `Signature` is immutable and deliberately small: it holds `.parameters`, it holds `.return_annotation`, and it knows how to render itself as source-like text. It is not the function, it does not let you call anything, and it is not the function's code object — it is a description of the *interface*. ### `.parameters` is an ordered read-only mapping `sig.parameters` is a `mappingproxy` — a read-only view over a dict — keyed by parameter name, in declaration order. You cannot mutate it; to produce a modified signature you build a new one with `Signature.replace()` or `Parameter.replace()`. Because it is a mapping and not a list, `sig.parameters["scale"]` is a direct lookup, while `sig.parameters.values()` walks the parameters left to right. ### What a `Parameter` carries Each value is an `inspect.Parameter` with four attributes worth knowing: * `.name` — the identifier as written. * `.kind` — how the argument may be supplied. * `.default` — the declared default value. * `.annotation` — the declared annotation. `.kind` is the interesting one, and it takes exactly five values: | kind | how it may be passed | |---|---| | `POSITIONAL_ONLY` | by position only; the name is not usable as a keyword | | `POSITIONAL_OR_KEYWORD` | the ordinary case — either way | | `VAR_POSITIONAL` | the catch-all that absorbs surplus positional arguments | | `KEYWORD_ONLY` | by keyword only | | `VAR_KEYWORD` | the catch-all that absorbs surplus keyword arguments | Those five values are the whole calling grammar of a Python callable, which is why introspection code branches on `.kind` far more often than on `.name`. ### `empty`, not `None` If a parameter declares no default, `.default` is `inspect.Parameter.empty`, a private sentinel class, and the same goes for `.annotation` and for `Signature.return_annotation`. This matters: `None` is an extremely common *real* default, so `if p.default is None` would misclassify `def f(x=None)` as "no default". The correct test is always `p.default is inspect.Parameter.empty`. ### What it works on `signature()` is the general-purpose entry point, and that generality is the reason to prefer it: * plain functions and lambdas; * bound methods — the already-bound first parameter is dropped, because that is how you actually call them; * classes — you get the constructor's calling convention; * instances of classes defining `__call__`; * `functools.partial` objects — already-supplied arguments are removed from the reported signature; * most C builtins, which ship a machine-readable text signature. A C callable that carries no introspection data raises `ValueError`; passing something that is not callable at all raises `TypeError`. Any code that introspects arbitrary user-supplied callables must handle both. ### It reports the convention, not the source text Two hooks can change what you see. A callable may set `__signature__` to declare a signature explicitly, and `signature()` honours it. `signature()` also follows a `__wrapped__` chain by default, so it reports the underlying callable rather than a thin forwarding layer; `follow_wrapped=False` turns that off. So the answer is best read as "how this object wants to be called", not "what the `def` line said". ### Cost and caching Building a `Signature` is real work — it inspects the code object, defaults and annotations, and constructs several objects. That is fine once at import or registration time and wasteful once per request. Introspect when you register a callable, keep the `Signature`, and reuse it. ### Python 3.14 Since 3.14, annotations are evaluated lazily (PEP 649/749) rather than at definition time, and `inspect.signature()` accepts an `annotation_format` argument so you can ask for annotations in string form instead of as evaluated objects. The parameter names, kinds and defaults you get back are unchanged; only how annotations are produced moved. ```python import inspect def render(path, /, pages=1, *, scale=1.0, **opts): ... sig = inspect.signature(render) print(sig) # (path, /, pages=1, *, scale=1.0, **opts) print(sig.parameters["scale"].default) # 1.0 print(sig.parameters["path"].default is inspect.Parameter.empty) # True ``` The practical takeaway: reach for `signature()` whenever you need to know how something may be called, branch on `.kind`, and compare defaults against `Parameter.empty`.
- Why does inspect.Parameter use an `empty` sentinel instead of None for a missing default?Because `None` is a perfectly ordinary default value. If `.default` were `None` for "no default", introspection code could not tell `def f(x)` from `def f(x=None)` — two callables with genuinely different behaviour. `inspect.Parameter.empty` is a value no user code would supply, so the test `p.default is inspect.Parameter.empty` is unambiguous. The same sentinel is used for `.annotation` and for `Signature.return_annotation`.
- What happens when you call inspect.signature() on a C builtin that carries no introspection data?It raises `ValueError` — for example `inspect.signature(time.strftime)` reports that no signature was found for that builtin. Most C callables now ship a machine-readable text signature and introspect fine, but a minority do not. Code that introspects arbitrary callables should catch `ValueError`, and also `TypeError` for objects that are not callable at all, then fall back to a permissive default rather than crashing.
- How does inspect.signature() report the `self` parameter of a bound method?It drops it. A `Signature` describes how you call the object in front of you, and you do not pass `self` to a bound method — the instance is already attached. Introspecting the underlying function on the class instead does show `self`, because at that point it is an ordinary parameter you would have to supply.
saying these in an interview costs you the question
- Says sig.parameters is a list of parameter names
- Assumes a parameter with no default reports None
- Thinks inspect.signature works on every callable without error
- Confuses the Signature object with the function's code object
- Believes the parameter mapping has no guaranteed order
- Tries to mutate sig.parameters in place