python-3.xreflectioninspect

Python `inspect.Signature` shows all defined positional arguments as `ParameterKind.POSITIONAL_OR_KEYWORD`


I had the impression that you could use the inspect.Signature function to retrieve and distinguish between positional and keyword arguments. However this does not seem to be the case:

def foo(a,b,c, t=3, q=5):
    print(a,b,c,t,q)

[(u, u.kind) for u in i.signature(foo).parameters.values()]

[(<Parameter "a">, <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>),
 (<Parameter "b">, <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>),
 (<Parameter "c">, <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>),
 (<Parameter "t=3">, <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>),
 (<Parameter "q=5">, <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>)]

So, as it is, seems the kind attribute is pretty useless to distinguish between positional and keyword arguments

So the question here is:

How do I distinguish between arguments a,b,c and arguments t,q so that if I were to invoke foo:

  #build args from signature with args = [a,b,c]
  #build kwargs from signature with kwargs = {'t': t,'q': q}
  foo(*args, **kwargs)

Solution

  • TL;DR

    Unless you explicitly define function parameters as being POSITIONAL_ONLY or KEYWORD_ONLY using the syntax below, the default behavior is for all parameters to be POSITIONAL_OR_KEYWORD. A difference exists between the behavior of Python 3.8+ and previous versions of Python.

    Python 3.8+

    In Python 3.8+ whether an argument is positional only or keyword only can be specified using the / and * syntax, respectively. As an example:

    def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
          -----------    ----------     ----------
          |              |              |
          |              Positional or  |
          |              keyword        Keyword only
          Positional only
    

    Everything before / is positional only; everything after * is keyword only. Note that order matters – / must come before *. Also, if you don't explicitly specify POSITIONAL_ONLY or KEYWORD_ONLY using this syntax, all arguments default to the POSITIONAL_OR_KEYWORD value for the kind attribute.

    This is due to a change that was made in Python 3.8. The behavior of the / syntax was specified in PEP 570 (following PEP 457). In code:

    >>> import inspect
    
    # Positional or keyword (default behavior)
    >>> def meow (a, b, c = 0, d = 1):
    ...    return (a * b + c) * d
    
    >>> {p.name: p.kind for p in inspect.signature(meow).parameters.values()}
    {'a': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>, 
     'b': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>, 
     'c': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>, 
     'd': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>}
    
    # Positional only, positional or keyword, keyword only
    >>> def meow (a, /, b, c = 0, *, d = 1):
    ...    return (a * b + c) * d
    
    >>> {p.name: p.kind for p in inspect.signature(meow).parameters.values()}
    {'a': <_ParameterKind.POSITIONAL_ONLY: 1>, 
     'b': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>, 
     'c': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>, 
     'd': <_ParameterKind.KEYWORD_ONLY: 1>}
    

    Before Python 3.8

    Prior to PEP 570, the / syntax did not exist but the * syntax did (haven't been able to find the exact PEP when it was introduced); trying the / in 3.7 raises a syntax error:

    # Python 3.7 - we get an error if use the `/` syntax
    >>> def meow (a, /, b, c = 0, *, d = 1):
      File "<stdin>", line 1
        def meow (a, /, b, c = 0, *, d = 1):
    
    # If we omit the `/` but keep the `*`, it works
    >>> def meow (a, b, c = 0, *, d = 1):
    ...    return (a * b + c) * d
    
    >>> {p.name: p.kind for p in inspect.signature(meow).parameters.values()}
    {'a': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>, 
     'b': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>, 
     'c': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>, 
     'd': <_ParameterKind.KEYWORD_ONLY: 1>}
    

    Aside from the PEPs, I also found this quick summary helpful to understanding the behavior.