Passing kwargs through mutliple levels of functions, unpacking some of them but passing all of them

Viewed 59

I have a lot of functions within functions and a lot of variables because I'm making modular 2D drawings. For that purpose I want to be able to pass a kwargs dict down into several layers of functions. As an example:

mydict = {'a':1,'b':2,'c':3}

def bar(b, c, **kwargs):
    print("bar b=" + str(b))
    print("bar c=" + str(c))
    print("bar kwargs: ", kwargs)


def foo(a, b, **kwargs):
    print("foo a=" + str(a))
    print("foo b=" + str(b))
    print("foo kwargs: ", kwargs)
    bar(b, **kwargs)

foo(**mydict)

This returns:

foo a=1
foo b=2
foo kwargs:  {'c': 3}
bar b=2
bar c=3
bar kwargs:  {}

This works nicely because it catches all the kwargs I don't need. However, I need to explicitly pass kwargs that I unpacked in foo to bar. I would like to pass the whole mydict down into bar as I did in foo while keeping it readable.

The only way I can think of doing that right now is by passing mydict twice, where I only unpack it once and catch any unused kwargs in **_, which doesn't seem nice.

mydict = {'a':1,'b':2,'c':3}

def bar(mydict, b, c, **_):
    print("bar b=" + str(b))
    print("bar c=" + str(c))
    print("bar \'kwargs\': ", mydict)

def foo(mydict, a, b, **_):
    print("foo a=" + str(a))
    print("foo b=" + str(b))
    print("foo \'kwargs\': ", mydict)
    bar(mydict, **mydict)

foo(mydict, **mydict)

Which returns

foo a=1
foo b=2
foo 'kwargs':  {'a': 1, 'b': 2, 'c': 3}
bar b=2
bar c=3
bar 'kwargs':  {'a': 1, 'b': 2, 'c': 3}

Are there better ways to do this?

4 Answers

Let the functions take only **kwargs:

mydict = {'a':1,'b':2,'c':3}

def bar(**kwargs):
    print("bar b=" + str(kwargs['b']))
    print("bar c=" + str(kwargs['c']))
    print("bar kwargs: ", kwargs)


def foo(**kwargs):
    print("foo a=" + str(kwargs['a']))
    print("foo b=" + str(kwargs['b']))
    print("foo kwargs: ", kwargs)
    bar(**kwargs)

foo(**mydict)

I think you've correctly identified a code smell in the architecture of your code.

(Not Recommended) Function for extracting variables from kwargs

There are inbuilt methods for a dict

b = mydict.get("b") # or mydict['b']

or maybe with a more bespoke function

def get_b_c(b, c, **_):
    return b, c

def bar(**kwargs):
    b,c = get_b_c(**kwargs)
    print("bar b=" + str(b))
    print("bar c=" + str(c))
    print("bar \'kwargs\': ", kwargs)

Using a class

The problem you have identified is needing to repeatedly pass in the same arguments to do some processing on them, this can be simplified by using an object and some attached methods. The arguments are parsed only once in the initialization.

class MyDrawing:

    def __init__(self, **kwargs) -> None:
        self.a = kwargs.get('a')
        self.b = kwargs.get('b')
        self.c = kwargs.get('c')
        self.kwargs = kwargs # you probably don't need it, but here to match your example

    def bar(self):
        print("bar b=" + str(self.b))
        print("bar c=" + str(self.c))
        print("bar \'kwargs\': ", self.kwargs)

    def foo(self):
        print("foo a=" + str(self.a))
        print("foo b=" + str(self.b))
        print("foo \'kwargs\': ", self.kwargs)
        self.bar()

drawing = MyDrawing(**mydict)
drawing.foo()

if you don't mind some extra junk passing around, you can use locals at the beginning of your function to collect your arguments into a new dict and update it with the kwargs, and later pass that one to the next function

mydict = {'a':1,'b':2,'c':3}

def bar(b, c, **kwargs):
    print("bar b=" + str(b))
    print("bar c=" + str(c))
    print("bar kwargs: ", kwargs)


def foo(a, b, **kwargs):
    new_arg = locals()|kwargs
    print("foo a=" + str(a))
    print("foo b=" + str(b))
    print("foo kwargs: ", kwargs)
    bar(**new_arg)

foo(**mydict)

which result into

foo a=1
foo b=2
foo kwargs:  {'c': 3}
bar b=2
bar c=3
bar kwargs:  {'a': 1, 'kwargs': {'c': 3}}

for older version of python locals()|kwargs is equivalent to:

new_arg = {**locals(),**kwargs}`

or

new_arg = dict(locals())
new_arg.update(kwargs)
@magic_copying_kwargs
def foo(a, b, /, **kwargs):
    ...

where the magic_copying_kwargs decorator is defined like this:

import inspect

def magic_copying_kwargs(function):
    parameters = inspect.signature(function).parameters
    def _wrapper_function(**kwargs):
        args = []
        for name in parameters:
            if name != 'kwargs':
                try:
                    args.append(kwargs[name])
                except KeyError:
                    raise TypeError(f'missing a required argument: {name!r}')
        return function(*args, **kwargs)
    return _wrapper_function

This assumes Python >=3.8, which has positional-only arguments.

Related