Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to type mutable default arguments

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.)

like image 474
Pro Q Avatar asked Jun 04 '21 01:06

Pro Q


People also ask

What is a mutable default argument?

In Python, when passing a mutable value as a default argument in a function, the default argument is mutated anytime that value is mutated. Here, "mutable value" refers to anything such as a list, a dictionnary or even a class instance.

Why does Python have mutable default arguments?

Python's default arguments are evaluated once when the function is defined, not each time the function is called (like it is in say, Ruby). This means that if you use a mutable default argument and mutate it, you will and have mutated that object for all future calls to the function as well.

What is a mutable parameter?

Mutable Parameters: It means that when a parameter is passed to the function using the caller function, then its value is bound to the parameter in the called function, which means any changes done to the value in that function will also be reflected in the parameter of the caller function.

Is none mutable in Python?

None is not mutable. The problem occurs when you modify the object that the parameter has as a default.


3 Answers

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 [].

like image 73
chepner Avatar answered Oct 20 '22 15:10

chepner


I'm not sure what's the issue here, since using Optional[List[int]] as the type is perfectly fine in mypy: https://mypy-play.net/?mypy=latest&python=3.9&gist=2ee728ee903cbd0adea144ce66efe3ab

In your case, when mypy sees bar = [] if bar is None else bar, it is smart enough to realize that bar cannot be None beyond this point, and thus narrow the type to List[int]. Read more about type narrowing in mypy here: https://mypy.readthedocs.io/en/stable/kinds_of_types.html?highlight=narrow#union-types

Here's some other examples of type narrowing:

from typing import *

a: Optional[int]
assert a is not None
reveal_type(a)  # builtins.int

b: Union[int, float, str]
if isinstance(b, int):
    reveal_type(b)  # builtins.int
else:
    reveal_type(b)  # Union[builtins.float, builtins.str]
like image 40
Zecong Hu Avatar answered Oct 20 '22 13:10

Zecong Hu


Why not just cut out the cast when you shadow bar:

def foo(bar: Optional[List[int]]=None):
    bar : List[int] = [] if bar is None else bar
    return sorted(bar)
like image 37
Kraigolas Avatar answered Oct 20 '22 14:10

Kraigolas