Modifying default arguments
Python is an amazing language with a boundless expression power. But with the power come responsibility. There are many quirks and traps lying around in Python, and even after many years with it, I still fall in some of them. Mostly I fall in tests, as they abuse my application much harder than actual users, but still…
Todays topic is default arguments for functions.
Intro
When we define a function we can specify a default value for it’s argument(s):
def foo(arg1=1, arg2=”two”):
passWe can specify any value as the default argument. For example, we can use an empty dict as the value:
def foo(arg={}):
passSo far so good.
Tricky defaults
Now let’s look to this innocent code:
class Foo(object):
def __init__(self, arg={}):
self.value = argdef add(self, key, value):
self.value[key] = value
Now let’s use it:
_ = Foo()
_.add(1, 1)
wtf = Foo()
print(wtf.value)I expected wtf.value to be {}. Because we have __init__, and it says self.value = arg, and arg is {}.
But both python3 and python2 insist that output is ‘{1:1}’.
Why?
Shallow copy and default args
When we use self.value = arg, we say that self.value shoudl point to the same storage object as args. After add function updates self.value, both args (in __init__ function) and self.value point to the same container. Which was updated to contain a pair {1:1}.
Next time we create instance of Foo without the argument, it calls __init__, and args now is pointing to… well, I really want to say {}… harsh python truth is that args now is pointing to a container, which looks like {} but contains {1:1}.
I find this behavior absolutely counter-intuitive. My code change something inside function in such a way that I couldn’t inspect it, I couldn’t name it, I couldn’t do anything with it. It’s like sprintf function in C, lying low in the bushes to jump out. You just NEED TO BE CAREFUL with it. And I hate this.
Solution
Never use shallow-copied objects (dict, list, set, tuple, etc) as default values for functions. Use None and add code to handle it:
def __init__(self, arg=None):
if arg is None:
self.value = {}
else:
self.value = argUgly? Ugly! Do we love python? No, we don’t!
