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