4

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)
lurscher
  • 25,930
  • 29
  • 122
  • 185

1 Answers1

5

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.

Greenstick
  • 8,632
  • 1
  • 24
  • 29