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.