def append_to(element, to=[]):
to.append(element)return to
0) append_to(
[0]
Contrarian view on mutable default arguments.
A. Coady
June 28, 2020
The use of mutable defaults is probably the most infamous Python gotcha. Default values are evaluated at definition time, which means mutating them will be persistent across multiple calls. Many articles on this topic even use the same append
example.
And the solution is invariably to use None
instead, and convert as needed.
There is another solution to the problem of mutating a default value: don’t do that. More specifically, the problem isn’t using mutables as defaults; the problem is actually mutating them.
If the input from the caller is being mutated, then the caller doesn’t need it returned because the caller already has a reference. This distinction is explicitly encouraged in Python, e.g., list.sort
vs. sorted
. But it follows that if the input doesn’t need to be returned, then there’s no point in the input being optional. How would the caller know the difference?
The reason why examples like the fluent append
seem so contrived is because they are. No one actually wants a function named append
to take one argument. The realistic fix would be:
Sure, there are rare occassions where a parameter is mutable but optional, such as a recursive algorithm that’s implicitly passing around its own cache. But this author would wager that given any real-world code that’s been bitten by this gotcha there is:
What harm does this advice do? Well, it’s caused an over-reaction resulting in using None
as the only default, even for immutables. It’s so prevalent that it appears many beginners believe using None
is the one and only way of making an argument optional.
Besides immutable types, there are also cases where mutation is irrelevant. Consider the following example adapted from a popular project.
Notice that the correctness of this code relies on the member list being newly created in either case. What could possibly go wrong with:
Or better yet, why not support the more liberal interface.
The point is that there are at least 4 solutions to this problem:
None
for mutables, and matching types for immutablesNone
for all defaultsOnly #1 is even remotely controversial, yet somehow the status quo has landed on #4. Besides being needlessly verbose, it has another pitfall. Python doesn’t natively support detecting where the argument was actually passed; a sentinel default is required for that. The implementation detail is leaking through the interface, indicating to the caller that None
is an acceptable argument to pass explicitly. As if the type hint was Optional[List]
, even though that’s not the intention. Factor in using **kwargs
- which clearly doesn’t want data padded with nulls - and actual code breakage can result.
Presumably the disdain for option #1 is because it might encourage the gotcha. But it’s disingenous to just let that go unsaid. The implementer is responsible for writing correct code, and the caller sees the right interface. The speculation is that beginners will read code which uses mutable defaults but doesn’t mutate them, and follow the former pattern but not the latter.
As a community, let’s at least push towards option #3. Using empty strings and zeros as defaults is all upside.