How to call function with dict, while ignoring unexpected keyword arguments?

Question:

Something of the following sort. Imagine this case:

def some_function(a, b):
    return a + b
  
some_magical_workaround({"a": 1, "b": 2, "c": 3})  # returns 3

I can’t modify some_function to add a **kwargs parameter. How could I create a wrapper function some_magical_workaround which calls some_function as shown?

Also, some_magical_workaround may be used with other functions, and I don’t know beforehand what args are defined in the functions being used.

Asked By: caeus

||

Answers:

If you want to pass the entire dict to a wrapper function, you can do so, read the keys internally, and pass them along too

def wrapper_for_some_function(source_dict):
    # two possible choices to get the keys from the dict
    a = source_dict["a"]         # indexing: exactly a or KeyError
    b = source_dict.get("b", 7)  # .get method: 7 if there's no b
    # now pass the values from the dict to the wrapped function
    return some_function(a, b)

original answer components

If instead, you can unpack the dict, when you define your function, you can add a **kwargs argument to eat up unknown args

def some_function(a, b, **kwargs):
    return a + b
>>> def some_function(a, b, **kwargs):
...     return a + b
... 
>>> d = {"a":1,"b":2,"c":3}
>>> some_function(**d)       # dictionary unpacking
3
Answered By: ti7

You can use the inspect module to find out the argument names for the original function and filter your dictionary:

import inspect


def some_function(a, b):
    return a + b


def some_magical_workaround(d):
    args = inspect.getfullargspec(some_function)[0]
    return some_function(**{k: v for k, v in d.items() if k in args})


print(some_magical_workaround({"a": 1, "b": 2, "c": 3}))

This will print:

3

It can even be made more general by making the function itself an argument:

def some_magical_workaround(func, d):
    args = inspect.getfullargspec(func)[0]
    ...

print(some_magical_workaround(some_function, {"a": 1, "b": 2, "c": 3}))
Answered By: Selcuk

So, you cannot do this in general if the function isn’t written in Python (e.g. many built-ins, functions from third-party libraries written as extensions in C) but you can use the inpsect module to introspect the signature. Here is a quick-and-dirty proof-of-concept, I haven’t really considered edge-cases, but this should get you going:

import inspect

def bind_exact_args(func, kwargs):
    sig = inspect.signature(func) # will fail with functions not written in Python, e.g. many built-ins
    common_keys = sig.parameters.keys() & kwargs.keys()
    return func(**{k:kwargs[k] for k in common_keys})

def some_function(a, b):
    return a + b

So, a demonstration:

>>> import inspect
>>>
>>> def bind_exact_args(func, kwargs):
...     sig = inspect.signature(func) # will fail with functions not written in Python, e.g. many built-ins
...     return func(**{k:kwargs[k] for k in sig.parameters.keys() & kwargs.keys()})
...
>>> def some_function(a, b):
...     return a + b
...
>>> bind_exact_args(some_function, {"a": 1, "b": 2, "c": 3})
3

But note how it can fail with built-ins:

>>> bind_exact_args(max, {"a": 1, "b": 2, "c": 3})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in bind_exact_args
  File "/usr/local/Cellar/[email protected]/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/inspect.py", line 3113, in signature
    return Signature.from_callable(obj, follow_wrapped=follow_wrapped)
  File "/usr/local/Cellar/[email protected]/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/inspect.py", line 2862, in from_callable
    return _signature_from_callable(obj, sigcls=cls,
  File "/usr/local/Cellar/[email protected]/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/inspect.py", line 2329, in _signature_from_callable
    return _signature_from_builtin(sigcls, obj,
  File "/usr/local/Cellar/[email protected]/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/inspect.py", line 2147, in _signature_from_builtin
    raise ValueError("no signature found for builtin {!r}".format(func))
ValueError: no signature found for builtin <built-in function max>

As noted in @JamieDoornbos answer, another example that will not work is a function with positional-only paramters:

E.g.:

def some_function(a, b, /, c):
    return a + b + c

Although, you can introspect this:

>>> def some_function(a, b, /, c):
...     return a + b + c
...
>>> sig = inspect.signature(some_function)
>>> sig.parameters['a'].kind
<_ParameterKind.POSITIONAL_ONLY: 0>
>>> sig.parameters['b'].kind
<_ParameterKind.POSITIONAL_ONLY: 0>
>>> sig.parameters['c'].kind
<_ParameterKind.POSITIONAL_OR_KEYWORD: 1>

If you need to handle this case, it is certainly possible to, but I leave that as an exercise to the reader 🙂

Answered By: juanpa.arrivillaga

For basic use cases, you can do something like this:

import inspect

def some_magical_workaround(fn, params):
    return fn(**{
        name: value for name, value in params.items()
        if name in set(inspect.getfullargspec(fn)[0])
    })

some_magical_workaround(some_function, {"a":1,"b":2,"c":3})

However, be warned that calling functions this way circumvents the explicit coupling of parameters, which is designed to expose errors earlier in the development process.

And there are some constraints on the values of fn that will work as expected. some_function is fine, but here’s an alternative that won’t work because the parameters are not allowed to have names:

>>> def some_function2(a, b, /):
...    return a+b
...
>>> some_magical_workaround(some_function2, {"a":1,"b":2,"c":3})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in apply_by_name
TypeError: some_function() got some positional-only arguments passed as keyword arguments: 'a, b'
Answered By: Jamie Doornbos
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.