What does inspect.signature() return for a Python function, and what can you read off it?
answer
- Not a list of names
- An object describing the call
- Ordered mapping of Parameter objects
- A sentinel marks 'nothing declared'
- Parameter.empty, never None
basics
~10 sinspect.signature() returns an immutable Signature object. Its .parameters attribute is an ordered, read-only mapping of names to inspect.Parameter objects, each carrying .name, .kind, .default and .annotation, and .return_annotation describes the return.
solid answer
~40 s`inspect.signature(func)` builds an `inspect.Signature` describing how the callable may be called. `sig.parameters` is a `mappingproxy` from parameter name to `inspect.Parameter`, in declaration order; each `Parameter` exposes `.name`, `.kind`, `.default` and `.annotation`, and `sig.return_annotation` covers the return. Where nothing was declared the value is the sentinel `inspect.Parameter.empty`, **not** `None` — `None` is a perfectly good real default, so you test with `is`. `Signature` and `Parameter` are immutable; `sig.replace(...)` and `param.replace(...)` hand back modified copies. It works on plain functions, on bound methods (with `self` already dropped), on classes (reporting the constructor), and on `functools.partial` objects (with the already-supplied parameters removed). Some C-implemented builtins carry no introspectable signature and raise `ValueError` — `inspect.signature(min)` is the classic example.
code
python · 10 linesimport inspect
def poll(sensor_id, window=60, *, retries=3):
return sensor_id
sig = inspect.signature(poll)
for p in sig.parameters.values():
has_default = p.default is not inspect.Parameter.empty
print(p.name, p.kind.name, p.default if has_default else "<no default>")
print(sig.return_annotation is inspect.Signature.empty)go deeper
Be ready to call inspect.signature on a function at the REPL and read parameter names, kinds and defaults out of .parameters. The one detail to memorise is that a missing default reads as Parameter.empty rather than None.
Explain the object model: an immutable Signature holding an ordered mappingproxy of Parameter objects, the empty sentinel, and .replace() for copies. Note that classes, bound methods and functools.partial objects each report a different effective signature.
Show where introspection fails in real code — builtins with no text signature raising ValueError, wrappers that hide the true parameters, annotations that may not be resolvable — and describe the fallback path your framework takes for callables it cannot introspect.
Own the question of how much of a framework's contract should rest on runtime introspection at all. Signature-driven dispatch reads beautifully but couples you to interpreter introspection quirks; an explicit registration API is duller and far more predictable to support.
## What the call actually produces `inspect.signature(callable)` does not return names, or a string, or a tuple. It returns one `inspect.Signature` object: a structured, immutable description of the *calling contract* of that callable. Everything else in the reflection story on this leaf — matching real arguments onto parameters, generating a CLI, injecting dependencies by name, rendering API docs — starts from this object. ## Signature.parameters The payload lives in `sig.parameters`. It is a `mappingproxy` (a read-only view over a dict) from parameter *name* to an `inspect.Parameter` object, iterated in declaration order. Read-only is deliberate: the whole object graph is immutable, so a framework can hold onto a `Signature` without another caller mutating it underneath. Each `Parameter` carries four attributes worth knowing on day one: * `.name` — the parameter's identifier as written in the `def`. * `.kind` — one of five values saying *how* an argument may be supplied (positional, keyword, or swept into a catch-all). * `.default` — the declared default value, as an object. * `.annotation` — the declared annotation. And on the `Signature` itself, `.return_annotation`. ## The empty sentinel The single most-missed detail: when there is no default and no annotation, the attribute is not `None`. It is the class-level sentinel `inspect.Parameter.empty` (the same object is exposed as `inspect.Signature.empty`). This exists precisely because `None` is an extremely common *real* default — `def poll(sink=None)` genuinely defaults to `None`, and if `empty` did not exist there would be no way to tell that apart from `def poll(sink)`. So the correct test is always identity: ```python if p.default is inspect.Parameter.empty: ... # this parameter is required ``` Writing `if p.default is None` or `if not p.default` is a bug that hides quietly until someone declares a default of `None`, `0` or `""`. ## Immutability and replace() You cannot assign into `sig.parameters` — it is a proxy, and both classes reject attribute assignment. To produce a variant you build a new list of `Parameter` objects and call `sig.replace(parameters=[...])`, or `sig.replace(return_annotation=...)`. `Parameter.replace(default=..., annotation=..., kind=...)` does the same one level down. Decorators that genuinely change a callable's contract use exactly this to publish an honest signature. ## What it accepts `signature()` is not limited to `def`-ed functions. It normalises several callable shapes: * a plain function — its own parameters; * a **bound** method — `self` already removed, because it is already supplied; * a **class** — the parameters of its constructor, minus `self`, which is what makes `inspect.signature(SomeClass)` show you how to build one; * any object with `__call__` — the parameters of that method; * a `functools.partial` — the parameters that remain unfilled, with already-supplied keywords appearing as new defaults. ## Where it fails Not every callable is introspectable. C-implemented builtins only report a signature if they carry a text signature; many do (`print`, `sys.exit`, `str.format`) but many do not, and those raise `ValueError: no signature found for builtin`. `inspect.signature(min)` and `inspect.signature(dict.update)` both raise on CPython 3.14. Any framework that introspects *arbitrary* user-supplied callables therefore needs a `try`/`except ValueError` fallback — usually "call it and let the interpreter validate the arguments instead". ## Annotations on 3.14 Since Python 3.14, annotations are evaluated lazily under PEP 649/749 rather than eagerly at `def` time. `inspect.signature()` still gives you *evaluated* annotation objects by default, so ordinary reflection code is unchanged; what is new is that you can ask for a different form via the `annotation_format` argument, using the formats from `annotationlib`, when evaluation would fail (an annotation naming a type that does not exist at runtime, for instance). The older `eval_str` argument, added in 3.10, resolves string annotations left over from `from __future__ import annotations`. ## Why interviewers ask it Because it is the entry point to every piece of framework code that has to work out what to pass a callable it did not write. Getting `Signature`, `Parameter`, `.kind` and `empty` straight is the vocabulary the rest of the subject is spoken in. ## Rendering and reusing a Signature `str(sig)` renders the signature the way it would appear in a `def` — `(*args, sep=' ', end='\n', file=None, flush=False)` for `print` — which makes it the cheapest possible way to produce a correct usage line in an error message or a log record, and far more reliable than assembling one from names by hand. `inspect.Signature.from_callable(obj)` is the classmethod form of the same construction, useful when you already hold the class. Because the object is immutable and holds no call state, it is also safe to compute once and keep: a framework that introspects a handler at registration time can store the `Signature` next to the handler and never introspect again.
- Why does inspect.Parameter.empty exist instead of just using None for a missing default?Because `None` is a common real default. If `.default` were `None` both for `def f(x)` and for `def f(x=None)`, no caller could tell a required parameter from one that genuinely defaults to `None`. `empty` is a unique sentinel object, so the test is `p.default is inspect.Parameter.empty`. The same sentinel marks a missing annotation and a missing return annotation.
- What happens when you call inspect.signature() on a C-implemented builtin?It depends on whether that builtin carries a text signature. Ones converted to the modern argument-parsing machinery — `print`, `sys.exit`, `str.format` — report a real `Signature`. Ones that have not been, such as `min` or `dict.update`, raise `ValueError: no signature found for builtin`. Code that introspects arbitrary callables must catch `ValueError` and degrade gracefully.
- How do you produce a modified copy of a Signature?With `Signature.replace()`. The objects are immutable and `sig.parameters` is a `mappingproxy`, so you build a new list of `Parameter` objects — each one `Parameter.replace()`-ed as needed — and call `sig.replace(parameters=[...])`, or `sig.replace(return_annotation=...)`. That is how a decorator publishes a signature that differs from the function it wraps.
A Signature is the form a caller has to fill in: one labelled field per parameter, each marked with how it may be filled and what goes there if you leave it blank.
saying these in an interview costs you the question
- Says signature() returns a list of parameter names
- Tests p.default == None to detect a missing default
- Assumes every callable has an introspectable signature
- Tries to assign into sig.parameters to change it
- Confuses the declared annotation with the argument's runtime type
- Thinks self appears in a bound method's signature