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

Question:

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.

Asked By: rspencer

||

Answers:

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
Answered By: Bas Swinckels

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
Answered By: Abhijit

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
Answered By: Aviendha

[@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)
Answered By: Menglong Li

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'
)
Answered By: Apiwat Chantawibul

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)

Answered By: Daniel Casas-Orozco
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.