Search code examples
pythonpython-internals

Python's inspect.Signature's signature seems to violate python's signatures rules


How did the inspect module make it seem like an argument doesn't have a default (though it does), and what would be the reason for doing this (if at all any)?

The help of inspect.Signature shows return_annotation argument without a default:

>>> import inspect
>>> help(inspect.Signature)
(...)
class Signature(builtins.object)
 |  Signature(parameters=None, *, return_annotation, __validate_parameters__=True)
(...)

Yet one can construct a Signature with only one argument:

>>> sig = inspect.Signature(None)
>>> # Nothing bad happens

This seems to violate the rules. So looking at the code for the Signature class, I see:

def __init__(self, parameters=None, *, return_annotation=_empty, __validate_parameters__=True)

Alright, return_annotation does have a default after all (which is why inspect.Signature(None) works).

So the questions are:

How did the inspect module achieve this trickery? Is there a good rational for doing so?


Solution

  • Apparently, help calls inspect.signature(object) under the hood, where object is the function. This treats the inspect.Signature.empty default argument in such a way that it's shown as having no default value at all.

    Another example:

    >>> def thing(self, a=inspect.Signature.empty):
    ...     ...
    ... 
    >>> help(thing)
    Help on function thing in module __main__:
    
    thing(self, a)  # Here it is!
    

    You can see what the help function does under the hood by running this:

    import trace
    trace.Trace().runfunc(help, thing)
    

    For me (Python 3.6) it shows this:

    <snip>
    pydoc.py(1357):             try:
    pydoc.py(1358):                 signature = inspect.signature(object)
     --- modulename: inspect, funcname: signature
    inspect.py(3002):     return Signature.from_callable(obj, follow_wrapped=follow_wrapped)
     --- modulename: inspect, funcname: from_callable
    inspect.py(2751):         return _signature_from_callable(obj, sigcls=cls,
    <snip>
    pydoc.py(1361):             if signature:
    pydoc.py(1362):                 argspec = str(signature)
    

    If you replicate this in your code, you'll get this:

    >>> inspect.signature(thing)
    <Signature (self, a)>
    >>> str(_)
    '(self, a)'  # huh?!