When do inspect.getfullargspec() and inspect.signature() disagree about a callable?
answer
- Two answers, two different shapes
- A flat tuple cannot carry every kind
- Defaults line up from the right
- One of them still lists the bound receiver
- The newer pair keeps data with its parameter
basics
~20 sgetfullargspec returns a flat FullArgSpec named tuple with no positional-only field, defaults aligned to the tail of args, and the bound first parameter of a method still listed. inspect.signature reports named Parameter objects and drops it.
solid answer
~40 s`inspect.getfullargspec()` returns a seven-field `FullArgSpec` named tuple — `args`, `varargs`, `varkw`, `defaults`, `kwonlyargs`, `kwonlydefaults`, `annotations` — and that flat shape cannot express everything a `Signature` can. There is no positional-only field, so parameters declared before a `/` are merged indistinguishably into `args`. `defaults` is a bare tuple aligned to the **last** entries of `args`, so mapping a default to its parameter is index arithmetic you must do yourself. For a bound method it still lists the already-bound first parameter, while `inspect.signature()` drops it because you do not pass it. It also ignores a `__wrapped__` chain that `signature()` follows by default. The raw `__defaults__` tuple and `__kwdefaults__` dict have the same shape problem, which is why the `Signature`/`Parameter` pair exists; new code should use `signature()`.
code
python · 11 linesimport inspect
class Renderer:
def emit(self, page, scale=1.0, *, dry_run=False):
...
bound_method = Renderer().emit
print(inspect.signature(bound_method))
print(inspect.getfullargspec(bound_method))
print(inspect.signature(dict.get))
print(inspect.getfullargspec(dict.get).args)go deeper
You will not be asked to choose between these, but recognise that the flat named-tuple API you may see in older code answers the same question as the modern Signature object and is not the one to write today.
Be able to name the shape difference — a seven-field tuple of parallel lists versus named Parameter objects — and explain the tail-aligned defaults tuple that forces index arithmetic on the caller.
Show that you know the traps that bite during migration: the bound first parameter counted on one side only, positional-only parameters invisible in the flat shape, and wrapper chains followed by one API and not the other.
Frame it as an API-design lesson worth reusing: parallel arrays that must be re-associated by index invite off-by-one bugs, and keeping each attribute attached to the entity it describes is what made the replacement worth shipping.
### Two APIs for one question Both functions answer "what parameters does this callable take?", and since Python 3.4 `getfullargspec()` has been implemented on top of the same machinery as `signature()`. They still disagree, because they disagree about the *shape* of the answer — and the older shape cannot represent everything modern Python can declare. ### The shape of `FullArgSpec` `inspect.getfullargspec(func)` returns a named tuple with exactly seven fields: `args`, `varargs`, `varkw`, `defaults`, `kwonlyargs`, `kwonlydefaults`, `annotations` `args` and `kwonlyargs` are lists of **names**. `defaults` is a tuple of **values**. `annotations` is a dict. Nothing binds a name to its own default except position. `inspect.signature(func)` returns a `Signature` whose `.parameters` is an ordered mapping from name to a `Parameter` object carrying its own `.kind`, `.default` and `.annotation`. The information travels together with the parameter it belongs to. ### Divergence 1 — defaults are tail-aligned In `FullArgSpec`, `defaults` lines up with the **last** `len(defaults)` entries of `args`. For `def render(path, scale=1.0)` you get `args=['path', 'scale']` and `defaults=(1.0,)`, and the association is left for you to compute — `dict(zip(args[len(args) - len(defaults):], defaults))`, with `defaults` being `None`, not `()`, when there are no defaults. Get that arithmetic wrong by one and you will confidently attribute a default to the wrong parameter. A `Parameter` simply carries `.default`, and uses `inspect.Parameter.empty` when there is none. This mirrors the raw function attributes: `__defaults__` is a tail-aligned tuple of positional defaults with no names attached, and `__kwdefaults__` is a separate dict keyed by keyword-only names. `FullArgSpec` is essentially those two attributes surfaced as a tuple, which is precisely the ergonomics problem `Signature` was introduced to fix. ### Divergence 2 — positional-only parameters vanish `FullArgSpec` has no field for positional-only parameters, so they are folded into `args` alongside ordinary ones. Introspect a builtin mapping's lookup method and `signature()` renders a trailing `/`, while `getfullargspec()` gives you a flat name list in which nothing marks the boundary. Any code that decides "can I pass this by keyword?" from `FullArgSpec` will answer yes for parameters that can only be passed positionally. ### Divergence 3 — the bound first parameter For a bound method, `getfullargspec()` lists `self` first; `signature()` omits it. Both are defensible: the older API describes the underlying function, the newer one describes how you call the object you have. But a loop that counts required arguments will be off by one across the two, and that off-by-one is the classic bug when migrating old introspection code. ### Divergence 4 — wrapper chains `signature()` follows a `__wrapped__` chain by default and can be told not to with `follow_wrapped=False`; `getfullargspec()` ignores `__wrapped__` entirely and reports the object in front of it. Both honour an explicit `__signature__` attribute, so that one is not a difference. ### Which to use `signature()`, in new code, without hesitation — it is more general (classes, `functools.partial` objects, instances with a call method), it represents all five parameter kinds, and it keeps each parameter's data attached to that parameter. `getfullargspec()` survives because a great deal of older introspection code was written against the flat tuple and unpacks it positionally. An even older variant of this API was removed in Python 3.11, so code that still imports it does not run on a current interpreter at all. Read `getfullargspec()` when you meet it; when you touch that code, migrate it — and when you do, check the two off-by-one traps first: the bound first parameter and the tail-aligned defaults. ### Python 3.14 Annotations are now evaluated lazily (PEP 649/749), so the `annotations` dict from either API is produced on demand rather than at definition time, and `inspect.signature()` gained an `annotation_format` argument for asking that annotations be returned as strings. Parameter names, kinds and defaults are unaffected — the divergences above are structural, not version-dependent.
- Why is the defaults field of FullArgSpec awkward to use correctly?It is a bare tuple of values aligned with the *last* entries of `args`, so recovering which parameter owns which default is index arithmetic the caller performs, and it is `None` rather than an empty tuple when there are no defaults. Keyword-only defaults live in a separate field entirely. A `Parameter` avoids all of this by carrying its own `.default`, using `inspect.Parameter.empty` to mean "none declared".
- Do the raw __defaults__ and __kwdefaults__ attributes have the same problem?Yes — `__defaults__` is a nameless tuple aligned to the tail of the positional parameters, and `__kwdefaults__` is a separate dict keyed by keyword-only names, so reconstructing "which parameter defaults to what" means the same arithmetic plus knowing the parameter order. They are the underlying storage; `inspect.signature()` is the layer that reassembles them into per-parameter objects for you.
- Is there any case where getfullargspec is the one you want?Rarely, and mostly by accident of context: it reports the object in front of it rather than following a `__wrapped__` chain, and it lists a bound method's receiver. If you specifically need one of those, `inspect.signature()` gives you the first with `follow_wrapped=False` and the second by introspecting the underlying function on the class, so new code has no real reason to reach for the older API.
saying these in an interview costs you the question
- Thinks FullArgSpec reports positional-only parameters distinctly
- Expects defaults to align with the start of args
- Assumes both APIs drop self for a bound method
- Believes getfullargspec no longer exists in current Python
- Says the two APIs are exact synonyms since 3.4
- Treats an empty defaults field as an empty tuple