Skip to content

Instantly share code, notes, and snippets.

@bitprophet
Forked from idan/gist:2403662
Created April 17, 2012 05:26
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bitprophet/2403699 to your computer and use it in GitHub Desktop.
Save bitprophet/2403699 to your computer and use it in GitHub Desktop.
Parameter Usage Style
>>> def init(headers={}):
... print "headers before: %r" % headers
... headers['foo'] = 'bar'
... print "headers after: %r" % headers
...
...
>>> init()
headers before: {}
headers after: {'foo': 'bar'}
>>> init()
headers before: {'foo': 'bar'}
headers after: {'foo': 'bar'}
>>>
>>> mydict = {}
>>> def init(headers=mydict):
... print "before: %r" % headers
... headers['foo'] = 'bar'
... print "after: %r" % headers
...
...
>>> print mydict
{}
>>> init()
before: {}
after: {'foo': 'bar'}
>>> print mydict
{'foo': 'bar'}
>>>
>>> def init(headers=None):
... headers = headers or {}
... print "before: %r" % headers
... headers['foo'] = 'bar'
... print "after: %r" % headers
...
...
>>> init()
before: {}
after: {'foo': 'bar'}
>>> init()
before: {}
after: {'foo': 'bar'}
>>>
@robbyt
Copy link

robbyt commented Apr 17, 2012

whoa... I don't understand what's happening on line 11 of bad.py. Isn't the headers variable local to the function?

@bitprophet
Copy link
Author

@robbyt No, because it's a mutable object (dict) and it was created at function definition time, not inside the function. (Same as with decorators, which execute at module load / function definition time and not when the function itself runs.)

Best explanation I could find quickly on Google is this SO question-and-answer (the first Edit: section explains the design decision.)

@bitprophet
Copy link
Author

I just added a 3rd example that makes this clearer by explicitly binding the default empty-dict keyword arg value, to a real module level name. As I understand it, both 'bad' examples are 100% the same, except in the first example the dictionary is "anonymous" at module level and has no name/binding. But in both cases, Python is allocating a dictionary object in memory at module load time, and that object then gets mutated anywhere it's passed around.

The reason there's no "new" internal variable inside the scope of the function is explained (I think) in that link I gave in my previous comment.

@donspaulding
Copy link

Trivial nitpick: line 2 of good.py will change any falsey value to an empty dict. In most cases that's OK, but it's a bit safer to say if headers is None: headers={} explicitly. It's more wordy to be sure, but people tripped up by the "non-local dict kwarg" ought to be made aware of this, too.

@bitprophet
Copy link
Author

@donspaulding Yes, that's always a potential gotcha, though in this case I felt it was worth using for example clarity, and because the kwarg is intended to be a dict -- another type of empty value is likely to be wrong anyways, and an empty dict would evaluate to the amusing but harmless expression {} or {}.

(You could argue that in a case where somebody gave e.g. 0 to a kwarg expecting a dict, we'd want it to ValueError or similar instead of silently casting to empty-dict, but I'm not sure that eventuality is worth changing things up for. Maybe use headers = {} if headers is None else headers, or just a two line `if block. A bit more verbose but also more correct.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment