How does one ignore unexpected keyword arguments passed to a function?

35,778

Solution 1

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

Solution 2

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

Solution 3

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

Solution 4

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'
)

Solution 5

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)

Share:
35,778
rspencer
Author by

rspencer

Updated on February 09, 2022

Comments

  • rspencer
    rspencer about 2 years

    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.