Splat-splatting Function Calls in Python 3 (or formally argument unpacking)

Python 3 supplies many different argument passing strategies for function calls. The basic ones are positional or keyword parameters and the ability to supply default values. We can also define functions with a variable number of both positional and/or keyword arguments.

This allows for great flexibility and for the implementation of methods/functions which can respond to a vast spectrum of cases.

Using varargs in positional fashion is quite straight-forward, and most people with some experience in other languages have already used this technique before. Combining methods accepting varargs and argument unpacking (also available in most languages) makes for a very fluid programming experience. What I did not know until recently is that we can actually apply the argument unpacking to our own custom objects (like special list objects, for instance).

Here’s a very simple example of a varargs function, which simply prints arguments in a single line (much like the built-in print :D):

>>> def print_args(*args):
...    print(" ".join(args))

>>> print_args("a", "pretty", "cat")
a pretty cat

Great, what about argument unpacking?
Argument unpacking is the ability to expand the contents of an ordered (or not) collection as arguments of a function call.
As an example, let’s take a look at two ways to transpose a matrix, one using nested list comprehensions and the other one with a simple zip and argument unpacking (example from python 3 tutorial):

>>> matrix = [
...     [1, 2, 3, 4],
...     [5, 6, 7, 8],
...     [9, 10, 11, 12],
... ]

>>> [ [row[i] for row in matrix] for i in range(len(matrix[0])) ]
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]

>>> list(zip(*matrix))
[(1, 5, 9), (2, 6, 10), (3, 7, 11), (4, 8, 12)]

In other languages the argument unpacking operator (an asterisk in case of Python and Ruby) is also referred to as “splat”. So, for the sake of brevity, I shall too use this naming convention, although it’s not really used by the python community. So, let’s splat:

>>> a_list = ["steals", "your", "food"]
>>> print_args(*a_list)
steals your food

Just like you can splat list elements into a varargs function, we can splat dictionaries into a var keyword arguments function:

>>> def print_with_labels(**kargs):
...    print(" ".join( v[0] + ":" + v[1] for v in kargs.items() ))

>>> a_dict = { "while": "you", "write": "code" }
>>> print_with_labels(**a_dict)
while:you write:code

Note the double asterisks, which stands for keyword args unpacking. Again, for brevity and clarity (and a bit of humour), I’ll call the double asterisk as splatsplat, as suggested by Josh Lee in on stackoverflow.

Note about dictionary keys ordering: In the above example I was lucky, and keys were passed to the method in the order I wanted generating a little phrase (or I may have tricked the example xD). However, since the standard dictionaries in Python 3 by default does not guarantee ordering, it could have been otherwise.

So, what’s the relation between a single asterisk operator and a double asterisk operator, if any? Is there any kind of recursion relation between splat and splatsplat? Can I play with associativity of splatsplat ? Well, except for the last question (which I have to assume it was actually too much), the rest of inquiries revealed some tricky stuff.

So I started by playing a little bit with the REPL, and after a few tries I got this:

>>> print_with_labels(*a_dict)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: print_with_labels() takes 0 positional arguments but 2 were given

Calling the method using only a splat on a dictionary did not raise some kind of strange, runtime or syntax error, but, instead, a TypeError with an error message that tells me that my single-splatted dict generated 2 positional arguments in the actual function call. So, why not try the same with print_args?

>>> print_args(*a_dict)
while write

That’s interesting! By applying the single asterisk operator I actually passed the dictionary keys as positional arguments!

I wasn’t satisfied, so I decided to splat some animals:

>>> class FluffyCat:
...     pass
... 
>>> b = FluffyCat()
>>> print_args(*b)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: print_args() argument after * must be a sequence, not FluffyCat

So our interpreter is complaining that FluffyCat is *not* a *sequence*. I managed to solve this by extending collections.abc.Sequence, but that sounded strange since Python should actually require behavior implementation (Duck typing) and not specific class/inheritance.

Eager for answers, I decided I needed a sequence of cats that I could use as a splatted argument into print_args! And so I did:

>>> class CatsList():
...    def __init__(self, *initial_cats):
...       self._cats = initial_cats
...       self._index = len(initial_cats)
...    def __iter__(self):
...       self._index = len(self._cats)
...       return self
...    def __next__(self):
...       if self._index == 0:
...          raise StopIteration
...       self._index = self._index - 1
...       return self._cats[self._index]

>>> # And here is my cats list:
>>> cats = CatsList("Felix", "Ofelia", "Lidia", "Tigre")
>>> cats._cats
('Felix', 'Ofelia', 'Lidia', 'Tigre')
>>> # and now we can print it:
>>> print_args(*cats)
Tigre Lidia Ofelia Felix

My suspicion was correct: although the previous error message stated the object actually had to *be* a *Sequence*, the only thing that matters is whether or not the object quacks (behaves) like (ish) a Sequence.

What is the difference between FluffyCat and CatsList? CatsList implements the iterator protocol, and hence, the interpreter is able to resolve argument unpacking on it by calling iter() and next(). As you can see, we do not need to implement the full Sequence behaviour, with methods like __len__, __getitem__, etcettera.

This is the reason why applying a single argument unpacking operator to a dictionary actually yields it’s keys as arguments: dictionaries implement the iterator protocol over it’s keys (same reason for which you can write for key in my_dict: (…) ).

That’s it for splats! I hope this article was useful. Please leave your comments below :)

Disclaimer: no animals were harmed in the making of this post.