Optional Parameters and Mutability
April 5, 2022
Overview
Optional parameters define sensible default values to allow the caller to omit arguments and still execute the function. However, optional parameters are evaluated once when the module is loaded, not with each call to the function.
Mutable Optional Parameters
Optional parameters with mutable default values may lead to unwanted behaviour if the object is modified within the function. For example, consider a function that defines one optional parameter with a default value of an empty list (a mutable object). The function prints the memory location of the argument's object and appends the object with the value of this memory location.
def f(a=[]):
print(id(a))
a.append(id(a))
Before function execution, evaluating the parameter's default value in the calling environment returns a tuple with one element. This element is an empty list object that is stored at the memory location 4398459520
.
f.__defaults__
>>> ([],)
id(f.__defaults__[0])
>>> 4398459520
Subsequently, if we call this function with a list object as an argument, we see that the memory location of the list object (which is different to the default) is referenced within the function. After function excution, evaluating the parameter's default value in the calling environment returns a tuple with one element. This element is an empty list object identical to the original.
f([1])
>>> 4395579648
f.__defaults__
>>> ([],)
id(f.__defaults__[0])
>>> 4398459520
However, if we call this function with no argument (allowing the default value to be used), we see that the memory location of the default list object is referenced within the function. After function excution, evaluating the parameter's default value in the calling environment returns a tuple with one element. This element is a list object that now contains the memory location of the default list object. Therefore, the default value has been modified.
f()
>>> 4398459520
f.__defaults__
>>> ([4398459520],)
id(f.__defaults__[0])
>>> 4398459520
Immutable Optional Parameters
To avoid unwanted behaviour from optional parameters, use immutable objects as default values. If you still wish to use a mutable object by default, use a sentinel value that indicates no argument has been given and handle this sentinel value explicitly within the function body.
def f(a=None):
if a is None:
a = []
print(id(a))
a.append(id(a))
f.__defaults__
>>> (None,)
id(f.__defaults__[0])
>>> 4398459520
f([1])
>>> 4395579888
f.__defaults__
>>> (None,)
id(f.__defaults__[0])
>>> 4398459520
f()
>>> 4398459520
f.__defaults__
>>> (None,)
id(f.__defaults__[0])
>>> 4398459520