One of the oddest changes I came across in transitioning beekeeper to be compatible with both Python 2 and Python 3 was the following. As far as I can tell, there's no documentation anywhere on this; it just is what it is.

In Python 3, this is a valid function definition:

def my_function(variable, *args, other_arg=123, **kwargs):
    print(variable)
    print(args)
    print(other_arg)
    print(kwargs)

It makes sense. We've got a single argument, a collector for any other unnamed arguments, a keyword argument with a default, and a keyword collector. Pretty? No. Works? Yes.

Problem is, in Python 2, it's invalid syntax to have any collector before any named variable. You have to do something like this:

def my_other_function(variable, other_arg=123, *args, **kwargs):
    print(variable)
    print(args)
    print(other_arg)
    print(kwargs)

It's very, very close. And in most cases, it'll do just fine. The issue, though, is that now, with the named keyword argument before the *args collector, any unnamed variables that get put in the function call will fill in other_arg before filling into *args.

It looks something like this:

In the first case, I'm going to use the original construction:

>>> my_function(1, 2, 3, thingy='whatever')
1
(2, 3)
123
{'thingy': 'whatever'}

Look what happens when I use the Python 2 construction:

>>> my_other_function(1, 2, 3, thingy='whatever')
1
(3,)
2
{'thingy': 'whatever'}

Nasty. Let's try something else; maybe if we set the kwarg manually, it'll collect those unnamed arguments like we want:

>>> my_other_function(1, 2, 3, thingy='whatever', other_arg=123)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: my_other_function() got multiple values for keyword argument 'other_arg'

Well, that's a pain.

It's completely different. And, unfortunately, there's no native way to do what I want to do easily (that is, reserve all additional unnamed arguments for the *args collector, rather than apply them to the keyword argument I already have a default value for).

In fact, in Python 2, it is not possible to collect arbitrary unnamed arguments when there're keyword arguments with default values, because the unnamed variables you pass will always fill in those keyword arguments first.

Essentially, if that's something you want to do in a codebase that's cross-compatible, you'll have to do something like this:

def my_new_function(variable, *args, **kwargs):
    other_arg = kwargs.pop('other_arg', 123)
    print(variable)
    print(args)
    print(other_arg)
    print(kwargs)

If you thought the first one was ugly, then that's even worse. Besides having an otherwise-unnecessary line of code, the variable is no longer in the method signature, which means that documentation will be more opaque.

I'm glad Python 3 has the "correct" (or at least, more capable) behavior. And I'm looking forward to the eventual demise of Python 2, when the code I write for Python 3 can actually take advantage of it.