Modifying default arguments

George Shuklin
Aug 23, 2017 · 2 min read

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”):
pass

We can specify any value as the default argument. For example, we can use an empty dict as the value:

def foo(arg={}):
pass

So far so good.

Tricky defaults

Now let’s look to this innocent code:

class Foo(object):
def __init__(self, arg={}):
self.value = arg
def 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 = arg

Ugly? Ugly! Do we love python? No, we don’t!

)

George Shuklin

Written by

I work at Servers.com, most of my stories are about Ansible, Ceph, Python, Openstack and Linux.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade