Why Function Calls in Python Should Match Args and Kwargs

Intentionally Short Post Follows

One of the debates I’ve taken part in with python code is a strict adherance to matching function calls by argument and keyword argument. For example, I argue that for a function defined below, it should be called in the manners directly below it:

def func_with_args_and_kwargs(arg1, arg2, kwarg1=None, kwarg2=None):
    print "Got args %s" % ([arg1, arg2])
    print "Got kwargs %s" % ([kwarg1, kwarg2])
    print "\n"


# SHOULD be called like this
func_with_args_and_kwargs(1, 2, kwarg1="foo", kwarg2="bar")


# SHOULD NOT be called like this
func_with_args_and_kwargs(1, 2, "foo", "bar")

Why?

A huge portion, perhaps the majority, of coding on a project involves maintenance. Code is malleable and subject to change, and with python in particular, if you don’t match args and kwargs when you make function calls you can end up shooting yourself in the foot. Since Python is not a compiled language, you won’t have a compiler to catch your errors in some cases, and unless you’re a 100% thorough grepper (also not super reliable), you can end up creating changes that won’t throw errors but will instead creating logical problems.

Take our example:

def func_with_args_and_kwargs(arg1, arg2, kwarg1=None, kwarg2=None):
    print "Got args %s" % ([arg1, arg2])
    print "Got kwargs %s" % ([kwarg1, kwarg2])
    print "\n"


# Function call with bad practice:
func_with_args_and_kwargs(1, 2, "foo", "bar")

Then let’s say we add an additional argument:

def func_with_args_and_kwargs(arg1, arg2, NEW_ARGUMENT, kwarg1=None, kwarg2=None):
    print "Got args %s" % ([arg1, arg2])
    print "Got kwargs %s" % ([kwarg1, kwarg2])
    print "\n"


# Function call with bad practice:
func_with_args_and_kwargs(1, 2, "foo", "bar")

The original function call will still work and run, but the initial intent with the arguments passed no longer match up. So you can see the difference in output:

Output 1:

Got args [1, 2]
Got kwargs ['foo', 'bar']

Output 2:

Got args [1, 2]
Got kwargs ['bar', None]

But, both versions of the program run. This is an oversimplified example of a case where you might modify a function, edit every function call that’s using it, but alas, some goofball on your team did something like this:

renamed_func = func_with_args_and_kwargs
renamed_func(1, 2, "foo", "bar")

And now your grep for “func_with_args_and_kwargs(” failed. And you didn’t write thorough tests. And now you created a bug in production that cost tens of thousands of dollars, and even your most jovial spirit combined with the best of jokes won’t re-kindle your good customer relations.

But Lobbdiesel, What if I Just Use Keyword Arguments All the Time?

That’s what they called me back in the Great War: Lobbdiesel. But I digress.

To counter my initial example, you could also do this:

def func_with_args_and_kwargs(arg1, arg2, kwarg1=None, kwarg2=None):
    print "Got args %s" % ([arg1, arg2])
    print "Got kwargs %s" % ([kwarg1, kwarg2])
    print "\n"


func_with_args_and_kwargs(arg1=1, arg2=2, kwarg1="foo", kwarg2="bar")

And you could indeed do that, and I’m hard-pressed to find an instance where you’d shoot yourself in the foot. However, in using nothing but keyword arguments you’ve incurred a large degree of unnecessary maintenance that are intrinsically not typically necessary. For example, what if I wanted to modify the argument names in the function?

def func_with_args_and_kwargs(awesome_new_arg_name1, awesome_new_arg_name2, kwarg1=None, kwarg2=None):
    pass

Now you’ll need to go and modify every function call that used keyword arguments in place of regular arguments. Or worse yet, what if for some reason you decide to rename each argument to a different argument?

def func_with_args_and_kwargs(arg2, arg1, kwarg1=None, kwarg2=None):
    pass

This is an unlikely scenario, but if you you modified the function like in the example above, in modifying the function you would still have an expectation of how the function was called. And what that expectation looks like varies if the caller is using all keyword arguments or just a conventional use of matching args and kwargs.

For example, these two function calls were identical before you made the change, but now they are not identical:

func_with_args_and_kwargs(1, 2, kwarg1="foo", kwarg2="bar")
func_with_args_and_kwargs(arg1=1, arg2=2, kwarg1="foo", kwarg2="bar")

The Case of Matching Args and Kwargs

I’ve tried to outline the cases for not matching args and kwargs, but we can at least examine the case of what expectations occur when you actually do match args and kwargs in Python:

  • You can move around keyword arguments in the function parameter with the reasonable expectation that it will not have adverse consequences
  • You can rename argument parameters to better defined names without adverse consequences, and the expectation is maintained that renaming keyword argument parameters will required that you modify the calls to that function
  • “Explicit is better than implicit” -The Zen of Python. Argument parameters are explicitly required. Keyword argument parameters are explicitly optional.

And Finally

Beyond the debate of args and kwargs, there’s the simple matter of how many parameters you use to define a function. A perhaps polarizing topic in Robert Martin’s book Clean Code which I found thought provoking was that more than one parameter is generally bad. I don’t have the exact quote, but I think the general gist was that one parameter was ok, two was bad, three was awful, and four was atrocious. The reason is that a function should do exactly one thing and no more, and when you have multiple parameters this will likely imply that a function does more than one thing.

I bring that up in the context of arg and kwarg usage for the reason that the debate might become moot when you keep in mind that intensely clean code will likely have no keyword arguments at all.

In Conclusion

Anyway, this is one of those instances where this could become a heated topic, so my strongest arguments occur on my own blog, where I can just throw something out there. If you don’t like it you can throw it right back.

  • Jason Scheirer

    Python 3 has something that makes this a little less annoying: explicit, keyword-only arguments. See https://www.python.org/dev/peps/pep-3102/ for the reference, but basically if you define a function as

    def f(x, y, *, z=None):

    you can’t call the function as f(1, 2, 3), you _have to_ explictly set the kwarg and call it as f(1, 2, z=3)