Recap
Python evaluates default values for arguments/parameters ahead of time; they are "early-bound". This can cause problems in a few different ways. For example:
>>> import datetime, time
>>> def what_time_is_it(dt=datetime.datetime.now()): # chosen ahead of time!
... return f'It is now {dt.strftime("%H:%M:%S")}.'
...
>>>
>>> first = what_time_is_it()
>>> time.sleep(10) # Even if time elapses...
>>> what_time_is_it() == first # the reported time is the same!
True
The most common way the problem manifests, however, is when the argument to the function is mutable (for example, a list
), and gets mutated within the function's code. When this happens, changes will be "remembered", and thus "seen" on subsequent calls:
>>> def append_one_and_return(a_list=[]):
... a_list.append(1)
... return a_list
...
>>>
>>> append_one_and_return()
[1]
>>> append_one_and_return()
[1, 1]
>>> append_one_and_return()
[1, 1, 1]
Because a_list
was created ahead of time, every call to the function that uses the default value will use the same list object, which gets modified on each call, appending another 1
value.
This is a conscious design decision that can be exploited in some circumstances - although there are often better ways to solve those other problems. (Consider using functools.cache
or functools.lru_cache
for memoization, and functools.partial to bind function arguments.)
This also implies that methods of an instance cannot use an attribute of the instance as a default: at the time that the default value is determined, self
is not in scope, and the instance does not exist anyway:
>>> class Example:
... def function(self, arg=self):
... pass
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in Example
NameError: name 'self' is not defined
(The class Example
also doesn't exist yet, and the name Example
is also not in scope; therefore, class attributes will also not work here, even if we don't care about the mutability issue.)
Solutions
Using None
as a sentinel value
The standard, generally-considered-idiomatic approach is to use None
as the default value, and explicitly check for this value and replace it in the function's logic. Thus:
>>> def append_one_and_return_fixed(a_list=None):
... if a_list is None:
... a_list = []
... a_list.append(1)
... return a_list
...
>>> append_one_and_return_fixed([2]) # it works consistently with an argument
[2, 1]
>>> append_one_and_return_fixed([2])
[2, 1]
>>> append_one_and_return_fixed() # and also without an argument
[1]
>>> append_one_and_return_fixed()
[1]
This works because the code a_list = []
runs (if needed) when the function is called, not ahead of time - thus, it creates a new empty list every time. Therefore, this approach can also solve the datetime.now()
issue. It does mean that the function can't use a None
value for other purposes; however, this should not cause a problem in ordinary code.
Simply avoiding mutable defaults
If it is not necessary to modify the argument in order to implement the function's logic, because of the principle of command-query separation, it would be better to just not do that.
By this argument, append_one_and_return
is poorly designed to begin with: since the purpose is to display some modified version of the input, it should not also actually modify the caller's variable, but instead just create a new object for display purposes. This allows for using an immutable object, such as a tuple, for the default value. Thus:
def with_appended_one(a_sequence=()):
return [*a_sequence, 1]
This way will avoid modifying the input even when that input is explicitly provided:
>>> x = [1]
>>> with_appended_one(x)
[1, 1]
>>> x # not modified!
[1]
It works fine without an argument, even repeatedly:
>>> with_appended_one()
[1]
>>> with_appended_one()
[1]
And it has gained some flexibility:
>>> with_appended_one('example') # a string is a sequence of its characters.
['e', 'x', 'a', 'm', 'p', 'l', 'e', 1]
PEP 671
PEP 671 proposes to introduce new syntax to Python that would allow for explicit late binding of a parameter's default value. The proposed syntax is:
def append_and_show_future(a_list=>None): # note => instead of =
a_list.append(1)
print(a_list)
However, while this draft PEP proposed to introduce the feature in Python 3.12, that did not happen, and no such syntax is yet available. There has been some more recent discussion of the idea, but it seems unlikely to be supported by Python in the near future.