77

Suppose I have some function, f:

def f (a=None):
    print a

Now, if I have a dictionary such as dct = {"a":"Foo"}, I may call f(**dct) and get the result Foo printed.

However, suppose I have a dictionary dct2 = {"a":"Foo", "b":"Bar"}. If I call f(**dct2) I get a

TypeError: f() got an unexpected keyword argument 'b'

Fair enough. However, is there anyway to, in the definition of f or in the calling of it, tell Python to just ignore any keys that are not parameter names? Preferable a method that allows defaults to be specified.

barbsan
  • 3,418
  • 11
  • 21
  • 28
rspencer
  • 2,651
  • 2
  • 21
  • 29

6 Answers6

59

As an extension to the answer posted by @Bas, I would suggest to add the kwargs arguments (variable length keyword arguments) as the second parameter to the function

>>> def f (a=None, **kwargs):
    print a


>>> dct2 = {"a":"Foo", "b":"Bar"}
>>> f(**dct2)
Foo

This would necessarily suffice the case of

  1. to just ignore any keys that are not parameter names
  2. However, it lacks the default values of parameters, which is a nice feature that it would be nice to keep
tripleee
  • 175,061
  • 34
  • 275
  • 318
Abhijit
  • 62,056
  • 18
  • 131
  • 204
30

If you cannot change the function definition to take unspecified **kwargs, you can filter the dictionary you pass in by the keyword arguments using the argspec function in older versions of python or the signature inspection method in Python 3.6.

import inspect
def filter_dict(dict_to_filter, thing_with_kwargs):
    sig = inspect.signature(thing_with_kwargs)
    filter_keys = [param.name for param in sig.parameters.values() if param.kind == param.POSITIONAL_OR_KEYWORD]
    filtered_dict = {filter_key:dict_to_filter[filter_key] for filter_key in filter_keys}
    return filtered_dict

def myfunc(x=0):
    print(x)
mydict = {'x':2, 'y':3}
filtered_dict = filter_dict(mydict, myfunc)
myfunc(**filtered_dict) # 2
myfunc(x=3) # 3
Aviendha
  • 763
  • 6
  • 16
  • 1
    This approach helps in circumstances where someone else wrote the function specification and you cannot change it from the calling code. great suggestion. id="dmid://uu023filterdict1623010139" – dreftymac Jun 06 '21 at 20:10
  • this seems to be the way to go also for numba functions, where `**kwargs` are not supported – mcsoini Jun 15 '22 at 14:50
  • This does not work if `myfunc` has signature `(*args, **kwargs)`, which get resolved concrete by a function it wraps. – John Jiang Nov 06 '22 at 17:18
22

This can be done by using **kwargs, which allows you to collect all undefined keyword arguments in a dict:

def f(**kwargs):
    print kwargs['a']

Quick test:

In [2]: f(a=13, b=55)
13

EDIT If you still want to use default arguments, you keep the original argument with default value, but you just add the **kwargs to absorb all other arguments:

In [3]: def f(a='default_a', **kwargs):
   ...:     print a
   ...:     

In [4]: f(b=44, a=12)
12
In [5]: f(c=33)
default_a
Community
  • 1
  • 1
Bas Swinckels
  • 18,095
  • 3
  • 45
  • 62
  • 1
    The edit is a little misleading. The most important thing you can do with the second that you can't do with the first is take `a` as a positional argument. (Unless you want to take `*args` as well as `**kwargs` and reproduce the entire argument-binding mechanism yourself.) Just handling a default value, on the other hand, is trivial—`print kwargs.get('a', 'default_a')`. – abarnert Oct 22 '14 at 19:53
  • Indeed, using `kwargs.get()` would be another way of defining a default value. The nice thing about my solution is that you can see that the function has a default value just from the function header, without having to dig into the body of the function. That it now also takes positional arguments is some minor side effect. – Bas Swinckels Oct 22 '14 at 20:00
  • I think the API is a much bigger effect than the introspectability of that API. – abarnert Oct 22 '14 at 21:00
3

I addressed some points in @Menglong Li's answer and simplified the code.

import inspect
import functools

def ignore_unmatched_kwargs(f):
    """Make function ignore unmatched kwargs.
    
    If the function already has the catch all **kwargs, do nothing.
    """
    if contains_var_kwarg(f):
        return f
    
    @functools.wraps(f)
    def inner(*args, **kwargs):
        filtered_kwargs = {
            key: value
            for key, value in kwargs.items()
            if is_kwarg_of(key, f)
        }
        return f(*args, **filtered_kwargs)
    return inner

def contains_var_kwarg(f):
    return any(
        param.kind == inspect.Parameter.VAR_KEYWORD
        for param in inspect.signature(f).parameters.values()
    )

def is_kwarg_of(key, f):
    param = inspect.signature(f).parameters.get(key, False)
    return param and (
        param.kind is inspect.Parameter.KEYWORD_ONLY or
        param.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD
    )

Here are some test cases:

@ignore_unmatched_kwargs
def positional_or_keywords(x, y):
    return x, y

@ignore_unmatched_kwargs
def keyword_with_default(x, y, z = True):
    return x, y, z

@ignore_unmatched_kwargs
def variable_length(x, y, *args, **kwargs):
    return x, y, args,kwargs

@ignore_unmatched_kwargs
def keyword_only(x, *, y):
    return x, y

# these test should run without error
print(
    positional_or_keywords(x = 3, y = 5, z = 10),
    positional_or_keywords(3, y = 5),
    positional_or_keywords(3, 5),
    positional_or_keywords(3, 5, z = 10),
    keyword_with_default(2, 2),
    keyword_with_default(2, 2, z = False),
    keyword_with_default(2, 2, False),
    variable_length(2, 3, 5, 6, z = 3),
    keyword_only(1, y = 3),
    sep='\n'
)
# these test should raise an error
print(
    #positional_or_keywords(3, 5, 6, 4),
    #keyword_with_default(2, 2, 3, z = False),
    #keyword_only(1, 2),
    sep = '\n'
)
Apiwat Chantawibul
  • 1,271
  • 1
  • 10
  • 20
  • I like your answer, but the dict comprehension is not pretty. dict comprehensions should be short and easy to read. I would suggest moving `inspect.signature(f).parameters.items()` to a variable and to put the complex if condition into its own function. That would make the comprehension much easier to read – Neuron Feb 07 '22 at 17:36
  • 1
    @Neuron Thanks, I refactored so the code now literally reads how `filtered_kwargs` really just contains pairs from `kwargs` under some condition which is moved to separate function. – Apiwat Chantawibul Feb 08 '22 at 03:02
  • very pretty! :) – Neuron Feb 08 '22 at 07:52
2

I used Aviendha's idea to build my own. It is only tested for a very simple case but it might be useful for some people:

import inspect

def filter_dict(func, kwarg_dict):
    sign = inspect.signature(func).parameters.values()
    sign = set([val.name for val in sign])

    common_args = sign.intersection(kwarg_dict.keys())
    filtered_dict = {key: kwarg_dict[key] for key in common_args}

    return filtered_dict

Tested on this specific case:

def my_sum(first, second, opt=3):
    return first + second - opt

a = {'first': 1, 'second': 2, 'third': 3}

new_kwargs = filter_dict(my_sum, a)

The example returns new_args = {'first': 1, 'second': 2} which can then be passed to my_sum as my_sum(**new_args)

1

[@Aviendha's answer][1] is great. Based on their post, I wrote an enhanced version supporting the default value in function's keywords-arguments signature, in Python 3.6:

import inspect
from inspect import Parameter
import functools
from typing import Callable, Any


def ignore_unexpected_kwargs(func: Callable[..., Any]) -> Callable[..., Any]:
    def filter_kwargs(kwargs: dict) -> dict:
        sig = inspect.signature(func)
        # Parameter.VAR_KEYWORD - a dict of keyword arguments that aren't bound to any other
        if any(map(lambda p: p.kind == Parameter.VAR_KEYWORD, sig.parameters.values())):
            # if **kwargs exist, return directly
            return kwargs

        _params = list(filter(lambda p: p.kind in {Parameter.KEYWORD_ONLY, Parameter.POSITIONAL_OR_KEYWORD},
                              sig.parameters.values()))

        res_kwargs = {
            param.name: kwargs[param.name]
            for param in _params if param.name in kwargs
        }
        return res_kwargs

    @functools.wraps(func)
    def wrapper(*args, **kwargs) -> Any:
        kwargs = filter_kwargs(kwargs)
        return func(*args, **kwargs)

    return wrapper


if __name__ == "__main__":
    @ignore_unexpected_kwargs
    def foo(a, b=0, c=3):
        return a, b, c


    assert foo(0, 0, 0) == (0, 0, 0)
    assert foo(a=1, b=2, c=3) == (1, 2, 3)
    dct = {'a': 1, 'b': 2, 'd': 4}
    assert foo(**dct) == (1, 2, 3)


    @ignore_unexpected_kwargs
    def fuzz(*args):
        return sum(args)


    # testing will not change original logic
    assert fuzz(1, 2, 3) == 6


    @ignore_unexpected_kwargs
    def bar(a, b, **kwargs):
        return a, b, kwargs.get('c', 3), kwargs.get('d', 4)


    assert bar(**{'a': 1, 'b': 2, 'd': 4, 'e': 5}) == (1, 2, 3, 4)
Menglong Li
  • 2,177
  • 14
  • 19
  • 1
    I learned a lot from this answer, but it still has problems. For example, [1] it can't handle `run(1,2)` because `wrapper` doesn't aceept any positional arguments even though the original `def run` declares positionals. [2] doesn't handle KEYWORD_ONLY that is new in python 3 [3] doesn't recognise if the original function already have **kwargs. – Apiwat Chantawibul Sep 08 '20 at 05:12
  • In the case of providing value in both positional and keyword form, I let Python standard decide what error it should raise. The test case is `keyword_with_default(2,2,3,z=False)` in my answer. – Apiwat Chantawibul Sep 08 '20 at 09:43
  • @ Apiwat Chantawibul, I changed the codes so that it can be called like run(1, 2) – Menglong Li Feb 08 '22 at 10:02