In Python function parameters can have default values. These default values are expressions which are executed when the function is defined, i.e.
only once. The same default value will be used every time the function is called, thus modifying it will have an effect on every subsequent call. This
can create some very confusing bugs.
It is also a bad idea to store mutable default value in another object (ex: as an attribute). Multiple instances will then share the same value and
modifying one objet will modify all of them.
This rule raises an issue when:
- a default value is either modified in the function or assigned to anything else than a variable and it has one of the following types:
- builtins: set, dict, list.
- collections module: deque, UserList, ChainMap, Counter, OrderedDict,
defaultdict, UserDict.
- or when an attribute of a default value is assigned.
Noncompliant Code Example
In the following example, the parameter "param" has list()
as a default value. This list is created only once and then reused in every
call. Thus when it appends 'a'
to this list, the next call will have ['a']
as a default value.
def myfunction(param=list()): # Noncompliant.
param.append('a') # modification of the default value.
return param
print(myfunction()) # returns ['a']
print(myfunction()) # returns ['a', 'a']
print(myfunction()) # returns ['a', 'a', 'a']
In the following example the same list is used for multiple instances of MyClass.param.
class MyClass:
def __init__(self, param=list()): # Noncompliant.
self.param = param # The same list is used for every instance of MyClass
def process(self, value):
self.param.append(value) # modifying the same list
a1, a2 = (MyClass(), MyClass())
a1.process("value")
print(a1.param) # ['value']
print(a2.param) # ['value']
Compliant Solution
def myfunction(param=None):
if param is None:
param = list()
param.append('a')
return param
print(myfunction()) # returns ['a']
print(myfunction()) # returns ['a']
print(myfunction()) # returns ['a']
class MyClass:
def __init__(self, param=None):
if param is None:
self.param = list()
else:
self.param = param
def process(self, value):
self.param.append(value)
a1, a2 = (MyClass(), MyClass())
a1.process("value")
print(a1.param) # ['value']
print(a2.param) # []
Exceptions
In some very rare cases modifying a default value is ok. For example, default values can be used as a cache.
No issue will be raised when the parameter’s name contains "cache" or "memo" (as in memoization).
See