Listing variables expected by a function in Python?

Just as a side answer, I now use another approach to pass to functions the variables they expect: I pass them all.

What I mean is that I maintain a kind of global/shared dictionnary of variables in my root object (which is the parent of all other objects), eg:

shareddict = {'A': 0, 'B':'somestring'}

Then I simply pass this dict to any method of any other object that is to be called, just like this:

shareddict.update(call_to_func(**shareddict))

As you can see, we unpack all the keys/values in shareddict as keyword arguments to call_to_func(). We also update shareddict with the returned result, we'll see below why.

Now with this technic, I can simply and clearly define in my functions/methods if I need one or several variables from this dict:

my_method1(A=None, *args, **kwargs):
''' This method only computes on A '''
    new_A = Do_some_stuff(A)
    return {'A': new_A} # Return the new A in a dictionary to update the shared value of A in the shareddict

my_method2(B=None, *args, **kwargs):
''' This method only computes on B '''
    new_B = Do_some_stuff(B)
    return {'B': new_B} # Return the new B in a dictionary to update the shareddict

my_method3(A=None, B=None, *args, **kwargs):
''' This method swaps A and B, and then create a new variable C '''
    return {'A': B, 'B': A, 'C': 'a_new_variable'} # Here we will update both A and B and create the new variable C

As you can notice, all the methods above return a dict of variables, which will update the shareddict, and which will get passed along to other functions.

This technic has several advantages:

  • Quite simple to implement
  • Elegant way to maintain a shared list of variables but without using a global variable
  • Functions and methods clearly show in their definitions what they expect (but of course one caveat is that even mandatory variables will need to be set as a keyword argument with a default value such as None, which usually means that the variable is optional, but here it's not
  • The methods are inheritable and overloadable
  • Low memory footprint since the same shareddict is passed all along
  • The children functions/methods define what they need (bottom-up), instead of the root defining what arguments will be passed to children (top-down)
  • Very easy to create/update variables
  • Optionally, it's VERY easy to dump all those variables in a file, eg by using json.dumps(finaldict, sort_keys=True).

You can use either the inspect.signature() or inspect.getfullargspec() functions:

import inspect

argspec = inspect.getfullargspec(somefunction)
signature = inspect.signature(somefunction)

inspect.fullargspec returns a named tuple with 7 elements:

  • A list with the argument names
  • The name of the catchall *args parameter, if defined (None otherwise)
  • The name of the catchall **kwargs parameter, if defined (None otherwise)
  • A tuple with default values for the keyword arguments; they go with the last elements of the arguments; match these by length of the default values tuple.
  • A list of keyword-only parameter names
  • A dictionary of default values for the keyword-only parameter names, if any
  • and a dictionary containing the annotations

With inspect.signature() you get a Signature object, a rich object that models not only the above data as a more structured set of objects but also lets you bind values to parameters the same way a call to the function would.

Which one is better will depend on your use cases.

Demo:

>>> import inspect
>>> def foo(bar, baz, spam='eggs', *monty, python: "kwonly", spanish=42, **inquisition) -> "return annotation":
...     pass
... 
>>> inspect.getfullargspec(foo)
FullArgSpec(args=['bar', 'baz', 'spam'], varargs='monty', varkw='inquisition', defaults=('eggs',), kwonlyargs=['python', 'spanish'], kwonlydefaults={'spanish': 42}, annotations={'return': 'return annotation', 'python': 'kwonly'})
>>> signature = inspect.signature(foo)
>>> signature
<Signature (bar, baz, spam='eggs', *monty, python: 'kwonly', spanish=42, **inquisition) -> 'return annotation'>
>>> signature.parameters['python'].kind.description
'keyword-only'
>>> signature.bind('Eric', 'Idle', 'John', python='Cleese')
<BoundArguments (bar='Eric', baz='Idle', spam='John', python='Cleese')>

If you have a dictionary named values of possible parameter values, I'd use inspect.signature() and use the Signature.parameters mapping to match names:

posargs = [
    values[param.name]
    for param in signature.parameters.values()
    if param.kind is Parameter.POSITIONAL_ONLY
]
skip_kinds = {Parameter.POSITIONAL_ONLY, Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD}
kwargs = {
    param.name: values[param.name]
    for param in signature.parameters.values()
    if param.name in values and param.kind not in skip_kinds
}

The above gives you a list of values for the positional-only parameters, and a dictionary for the rest (excepting any *args or **kwargs parameters).