Stéphan Tulkens

NLP Person

kwargs.pop is probably a code smell

Sometimes I see something like this:

from typing import Any, Sequence

def frombulize(items: Sequence[str], **kwargs: Any) -> list[str]
    prepend = kwargs.pop("prepend", "")

    if prepend:
        return [f"{prepend}{x}" for x in items]

    return items

Using kwargs.pop in this way is weird, and should be avoided most of the time. This blog post is about how and why to avoid this pattern.

What is **kwargs?

**kwargs is a mapping from arbitrary keywords to arbitrary values which is unpacked (hence the **). It allows us to pass in arbitrary values with arbitrary keywords into our functions, which can make them much more flexible. This flexibility, however, comes at a cost, because we can no longer infer anything about the content of **kwargs.

As an example, the following calls to frombulize are all valid:

frombulize(items=["dog", "cat"], what=10, clown=3)
frombulize(items=["dog", "cat"], prepend="dog")

Note, however, that, even though items was specified as a keyword argument, it doesn’t enter **kwargs. Only keywords that are otherwise unspecified in the function signature get put into the mapping. See here:

def frombulize(a: int, **kwargs):
    print(kwargs)

frombulize(a=3, b=4)
# prints {'b': 4}
kwargs = {"a": 3, "b": 4}
frombulize(**kwargs)
# prints {'b': 4}

So: **kwargs allows you to pass any keyword argument to a function, but only arguments that weren’t specified explicitly in the function signature get put into **kwargs.

Working with **kwargs

Because of the above, the function can equivalently be written as:

from typing import Any, Sequence

def frombulize(items: Sequence[str], 
               prepend: str = "", 
               **kwargs: Any) -> list[str]
    if prepend:
        return [f"{prepend}{x}" for x in items]

    return items

Literally nothing changes about this function, except that it became much clearer. Besides this, there is another reason to not directly use **kwargs. Note that in the first definition of the function above, we used kwargs.pop. We do this, because we want to avoid that functions that are called by this function that also take **kwargs accidentally get passed the prepend keyword. But, as noted above, this is exactly what specifying the keyword does: by specifying the keyword, we no longer add prepend to **kwargs, and thus don’t pass it on to any other function.

If you do want to pass it on to another function, just add it to the function signature of the function below:

from typing import Any, Sequence

def frombulize(items: Sequence[str], 
               prepend: str = "", 
               **kwargs: Any) -> list[str]
    if prepend:
        return [f"{prepend}{x}" for x in items]
    
    items = my_other_func(prepend=prepend, **kwargs)

    return items

This has exactly the same functionality.

While I think using kwargs.pop has its uses, I think it is probably overused, and difficult to work with. One real reason to use it, is if your classes have a variable function that is called, and you need to pass parameters on to this function.

Newer >>