149

I am using PyCharm (Python 3) to write a Python function which accepts a dictionary as an argument with attachment={}.

def put_object(self, parent_object, connection_name, **data):
    ...

def put_wall_post(self, message, attachment={}, profile_id="me"):
    return self.put_object(profile_id, "feed", message=message, **attachment)

In the IDE, attachment={} is colored yellow. Moving the mouse over it shows a warning.

Default arguments value is mutable

This inspection detects when a mutable value as list or dictionary is detected in a default value for an argument.

Default argument values are evaluated only once at function definition time, which means that modifying the default value of the argument will affect all subsequent calls of the function.

What does this mean and how can I resolve it?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
bin
  • 1,625
  • 3
  • 12
  • 10
  • Do you want it to not be mutable? – Peter Wood Jan 16 '17 at 23:49
  • 3
    @juanpa.arrivillaga the user is asking about PyCharm's inspection giving him/her a compiler warning s/he was not expecting. – the_constant Jan 16 '17 at 23:50
  • 1
    @Vincenzzzochi yes, but what about it? Asking what does it mean? A non-mutable alternative? How do idiomatically have mutable default arguments that don't get retained on subsequent calls? There isn't enough detail in the question. – juanpa.arrivillaga Jan 16 '17 at 23:52
  • 4
    @juanpa.arrivillaga The user's question was "how do I make this warning go away?" We both know that there could potentially be a bug shown in his/her code, but the question asked was obvious, and you were trying to pry. – the_constant Jan 16 '17 at 23:55
  • 3
    @Vincenzzzochi you think I'm "prying"? how is asking for a well-formed question "prying"? If someone doesn't want questions about their question, they shouldn't ask questions. In any event, it is best for the OP to clarify what their question is exactly rather than making assumptions, even though in this case I believe you have a *reasonable* interpretation. – juanpa.arrivillaga Jan 17 '17 at 00:00
  • 11
    @juanpa.arrivillaga You're prying whether it's intentional or not. You either 1: don't understand their obvious question (reread it), or 2: asking them to supply an answer to their own question because you would rather feel high and mighty about them not understanding a fundamental python principle that you do understand. Your response to me has the tone of #2. We live in a world where basic implicit interpretation reigns king, my lad, you should give it a shot sometime – the_constant Jan 17 '17 at 00:05
  • Possible duplicate of [Why does using None fix Python's mutable default argument issue?](http://stackoverflow.com/questions/10676729/why-does-using-none-fix-pythons-mutable-default-argument-issue) – mkrieger1 Jan 17 '17 at 00:20
  • 1
    @Vincenzzzochi it's open to interpretation, maybe have a friendly chat with juanpa in [the chat room](https://chat.stackoverflow.com/rooms/6/python). – Peter Wood Jan 17 '17 at 07:52

6 Answers6

164

If you don't alter the "mutable default argument" or pass it anywhere where it could be altered just ignore the message, because there is nothing to be "fixed".

In your case you only unpack (which does an implicit copy) the "mutable default argument" - so you're safe.

If you want to "remove that warning message" you could use None as default and set it to {} when it's None:

def put_wall_post(self,message,attachment=None,profile_id="me"):
    if attachment is None:
        attachment = {}

    return self.put_object(profile_id,"feed",message = message,**attachment)

Just to explain the "what it means": Some types in Python are immutable (int, str, ...) others are mutable (like dict, set, list, ...). If you want to change immutable objects another object is created - but if you change mutable objects the object remains the same but it's contents are changed.

The tricky part is that class variables and default arguments are created when the function is loaded (and only once), that means that any changes to a "mutable default argument" or "mutable class variable" are permanent:

def func(key, value, a={}):
    a[key] = value
    return a

>>> print(func('a', 10))  # that's expected
{'a': 10}
>>> print(func('b', 20))  # that could be unexpected
{'b': 20, 'a': 10}

PyCharm probably shows this Warning because it's easy to get it wrong by accident (see for example Why do mutable default arguments remember mutations between function calls? and all linked questions). However, if you did it on purpose (Good uses for mutable function argument default values?) the Warning could be annoying.

Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
MSeifert
  • 145,886
  • 38
  • 333
  • 352
  • Would this affect variables defined in `__init__` upon class creation? For example: [`ElementTree.Element(tag, attrib={}, **extra)`](https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element) – Stevoisiak Feb 28 '18 at 17:18
  • @StevenVascellaro Yes. However the first thing they do is [`copy`ing it](https://github.com/python/cpython/blob/v3.6.4/Lib/xml/etree/ElementTree.py#L171). That way they work with a copy and don't risk mutating the default argument. – MSeifert Feb 28 '18 at 19:40
  • Or shorter type `attachment = attachment or {}` instead of `if attachment is None: attachment = {}` – Georgii Oleinikov Apr 04 '19 at 08:56
  • @GeorgiiOleinikov There are some subtle differences between those two, for example the `is None` approach would not silently convert false-y values to empty dictionaries (e.g. if someone passed in `False`). I would also go with the `or {}` approach but **also** add some documentation or type-hints so that it's obvious what should be passed in. Note that I also proposed that approach on the other answer (https://stackoverflow.com/questions/41686829/warning-about-mutable-default-argument-in-pycharm/41686973?noredirect=1#comment70571249_41686977) - but there both are actually equivalent. – MSeifert Apr 04 '19 at 09:07
15

You can replace mutable default arguments with None. Then check inside the function and assign the default:

def put_wall_post(self, message, attachment=None, profile_id="me"):
    attachment = attachment if attachment else {}

    return self.put_object(profile_id, "feed", message=message, **attachment)

This works because None evaluates to False so we then assign an empty dictionary.

In general you may want to explicitly check for None as other values could also evaluate to False, e.g. 0, '', set(), [], etc, are all False-y. If your default isn't 0 and is 5 for example, then you wouldn't want to stomp on 0 being passed as a valid parameter:

def function(param=None):
    param = 5 if param is None else param
Peter Wood
  • 23,859
  • 5
  • 60
  • 99
  • 21
    or even shorter `attachment = attachment or {}` – MSeifert Jan 17 '17 at 00:11
  • 3
    @MSeifert I never found that short circuit very readable, and I wouldn't expect others to understand my code if I used it. I think coming from a C++ background I expect Boolean expressions to produce true/false. Maybe I need to train myself to not be repulsed by it (c: – Peter Wood Jan 17 '17 at 07:43
  • 1
    Both versions have a problem. If the parameter is an empty string (or an empty list) the function will replace it with an empty dictionary. The might or might not be intended. – Matthias Jan 17 '17 at 08:16
  • 1
    @Matthias the function expects a dictionary. If you're passing it something else you have bigger problems. – Peter Wood Jan 17 '17 at 09:21
  • 2
    @PeterWood: Depending on the function I might be able to pass something else (keyword: _duck typing_). But you're right: in this special context the usage of a string or a list would be an error. – Matthias Jan 17 '17 at 09:38
  • @Matthias updated with a more general explanation, thanks. – Peter Wood Jan 17 '17 at 11:08
13

This is a warning from the interpreter that because your default argument is mutable, you might end up changing the default if you modify it in-place, which could lead to unexpected results in some cases. The default argument is really just a reference to the object you indicate, so much like when you alias a list to two different identifiers, e.g.,

>>> a={}
>>> b=a
>>> b['foo']='bar'
>>> a
{'foo': 'bar'}

if the object is changed through any reference, whether during that call to the function, a separate call, or even outside the function, it will affect future calls the function. If you're not expecting the behavior of the function to change at runtime, this could be a cause for bugs. Every time the function is called, it's the same name being bound to the same object. (in fact, I'm not sure if it even goes through the whole name binding process each time? I think it just gets another reference.)

The (likely unwanted) behavior

You can see the effect of this by declaring the following and calling it a few times:

>>> def mutable_default_arg (something = {'foo':1}):
    something['foo'] += 1
    print (something)


>>> mutable_default_arg()
{'foo': 2}
>>> mutable_default_arg()
{'foo': 3}

Wait, what? yes, because the object referenced by the argument doesn't change between calls, changing one of its elements changes the default. If you use an immutable type, you don't have to worry about this because it shouldn't be possible, under standard circumstances, to change an immutable's data. I don't know if this holds for user-defined classes, but that is why this is usually just addressed with "None" (that, and you only need it as a placeholder, nothing more. Why spend the extra RAM on something more complicated?)

Duct-taped problems...

In your case, you were saved by an implicit copy, as another answer pointed out, but it's never a good idea to rely on implicit behavior, especially unexpected implicit behavior, since it could change. That's why we say "explicit is better than implicit". Besides which, implicit behavior tends to hide what's going on, which could lead you or another programmer to removing the duct tape.

...with simple (permanent) solutions

You can avoid this bug magnet completely and satisfy the warning by, as others have suggested, using an immutable type such as None, checking for it at the start of the function, and if found, immediately replacing it before your function gets going:

def put_wall_post(self, message, attachment=None, profile_id="me"):
    if attachment is None:
        attachment = {}
    return self.put_object(profile_id, "feed", message=message, **attachment)

Since immutable types force you to replace them (Technically, you are binding a new object to the same name. In the above, the reference to None is overwritten when attachment is rebound to the new empty dictionary) instead of updating them, you know attachment will always start as None unless specified in the call parameters, thus avoiding the risk of unexpected changes to the default.

(As an aside, when in doubt whether an object is the same as another object, compare them with is or check id(object). The former can check whether two references refer to the same object, and the latter can be useful for debugging by printing a unique identifier—typically the memory location—for the object.)

3

To rephrase the warning: every call to this function, if it uses the default, will use the same object. So long as you never change that object, the fact that it is mutable won't matter. But if you do change it, then subsequent calls will start with the modified value, which is probably not what you want.

One solution to avoid this issue would be to have the default be a immutable type like None, and set the parameter to {} if that default is used:

def put_wall_post(self,message,attachment=None,profile_id="me"):
    if attachment==None:
        attachment={}
    return self.put_object(profile_id,"feed",message = message,**attachment)
Scott Hunter
  • 48,888
  • 12
  • 60
  • 101
0
  • Lists are mutable and as declaring default with def at declaration at compile time will assign a mutable list to the variable at some address

    def abc(a=[]):
        a.append(2)
        print(a)
    
    abc() #prints [2]
    abc() #prints [2, 2] as mutable thus changed the same assigned list at func delaration points to same address and append at the end
    abc([4]) #prints [4, 2] because new list is passed at a new address
    abc() #prints [2, 2, 2] took same assigned list and append at the end 
    

     

  • To correct this:

    def abc(a=None):
         if not a:
             a=[]
         a.append(2)
         print(a)
    

     

    • This works as every time a new list is created and not referencing the old list as a value always null thus assigning new list at new address
Jatin Verma
  • 363
  • 3
  • 12
0

PyCharm warns that the default argument is mutable, which may seem obtuse, but what it means is that objects you create using the default are all sharing the same reference to that one default argument.

Here's a bit of code that demonstrates the problem:

class foo:
def __init__(self, key, stuff: list = []):
    self.key = key
    self.stuff = stuff

def __str__(self):
    return f"{self.key} :: {self.stuff}"

def add_item(self, item):
    self.stuff.append(item)

If I then go on to create a few instances of the foo class without supplying a new list of stuff for each, then each instance will share a reference to that same default list!

a = foo('a')
b = foo('b')
print(a, b)
a.add_item(1)
a.add_item(2)
print(a, b)

>>> a :: [] b :: []
>>> a :: [1, 2] b :: [1, 2]

You can see I've added items to the stuff list for a but when I print the two instances the second time, b also has two items in it's stuff as well, ... in fact it's the same two items!

The best way to get around this, but still supply defaults is to change your code just a bit and supply None as the default, then use or to coalesce that with a new list inside the constructor:

class foo:
def __init__(self, key, stuff: list = None):
    self.key = key
    self.stuff = stuff or []  # this will be a new list[]

Now if we repeat the construction of a and b and add stuff to a exactly as before, we get a different result:

>>> a :: [] b :: []     # before adding stuff
>>> a :: [1, 2] b :: [] # after adding stuff to a,  b is still empty!

This is because the instance a and the instance b no longer share a reference to the same (default) list, but use new lists constructed when the instance was initialized. While python hides most of the ugliness of pointers and references they are still there under-the-hood and sometimes we still need to be aware of them. Incidentally, if you supply values (primitive types) as defaults, they don't have this issue because the value itself is placed in the instance, not a reference (e.g. stuff=1 rather than stuff=[]).

Steve L
  • 1,523
  • 3
  • 17
  • 24