The way to deal with mutable default arguments in Python is to set them to None.
For example:
def foo(bar=None):
bar = [] if bar is None else bar
return sorted(bar)
If I type in the function definition, then the only type for bar
says that bar
is Optional
when, clearly, it is not Optional
by the time I expect to run that sorted
function on it:
def foo(bar: Optional[List[int]]=None):
bar = [] if bar is None else bar
return sorted(bar) # bar cannot be `None` here
So then should I cast?
def foo(bar: Optional[List[int]]=None):
bar = [] if bar is None else bar
bar = cast(List[int], bar) # make it explicit that `bar` cannot be `None`
return sorted(bar)
Should I just hope that whoever reads through the function sees the standard pattern of dealing with default mutable arguments and understands that for the rest of the function, the argument should not be Optional
?
What's the best way to handle this?
EDIT:
To clarify, the user of this function should be able to call foo
as foo()
and foo(None)
and foo(bar=None)
. (I don't think it makes sense to have it any other way.)
EDIT #2:
Mypy will run with no errors if you never type bar
as Optional
and instead only type it as List[int]
, despite the default value being None
. However, this is highly not recommended because this behavior may change in the future, and it also implicitly types the parameter as Optional
. (See this for details.)
None
is not the only sentinel available. You can choose your own list value to use as a sentinel, replacing it (rather than None
) with a new empty list at run time.
_sentinel = []
def foo(bar: List[int]=_sentinel):
bar = [] if bar is _sentinel else bar
return sorted(bar)
As long as no one calls foo
using _sentinel
as an explicit argument, bar
will always get a fresh empty list. In a call like foo([])
, bar is _sentinel
will be false: the two empty lists are not the same object, as the mutability of lists means that you cannot have a single empty list that always gets referenced by []
.