There is a well-known gotcha in Python. Namely, the default arguments retaining the mutation if mutated.
Example from The Hitchhiker’s Guide to Python
def append_to(element, to=[]):
to.append(element)
return to
append_to(12)
my_other_list = append_to(42)
print(my_other_list)
Running this code will result in [12, 42]
being printed, due to the aforementioned mutation retention.
TLDR
You can:
from collections.abc import Mapping, Sequence, Set
from types import MappingProxyType
def fn(
set_: Set[int] = frozenset({1, 2, 3}),
list_: Sequence[str] = ("a", "b", "c"),
dict_: Mapping[str, int] = MappingProxyType({"a": 3}),
): ...
if you are not going to mutate the arguments. Types can, of course, be different.
Generally used solution
The proposed solution is kinda bolierplaty, and awkward to type:
def append_to(element, to=None):
if to is None:
to = []
to.append(element)
return to
This, of course, works, but becomes really awkward once you have more arguments and want typing.
def fn(
set_arg: set[int] | None = None,
list_arg: list[str] | None = None,
dict_arg: dict[str, int] | None = None,
):
if set_arg is None:
set_arg = {1,2,3}
if list_arg is None:
list_arg = ["a", "b", "c"]
if dict_arg is None:
dict_arg = {"a": 3}
And even after this the type checker will still tell you that each of those variables can be None
, and to avoid this you should initialise a new variable for each argument.
def fn(
set_arg: set[int] | None = None,
list_arg: list[str] | None = None,
dict_arg: dict[str, int] | None = None,
):
if set_arg is not None:
set_var = set_arg
else:
set_var = {1,2,3}
if list_arg is not None:
list_var = list_arg
else:
list_var = ["a", "b", "c"]
if dict_arg is not None:
dict_var = dict_arg
else:
dict_var = {"a": 3}
or less abhorrent:
def fn(
set_arg: set[int] | None = None,
list_arg: list[str] | None = None,
dict_arg: dict[str, int] | None = None,
):
set_var = set_arg if set_arg is not None else {1,2,3}
list_var = list_arg if list_arg is not None else ["a", "b", "c"]
dict_var = dict_arg if dict_arg is not None else {"a": 3}
Note that you cannot simply:
def bad_example(
set_arg: set[int] | None = None,
list_arg: list[str] | None = None,
dict_arg: dict[str, int] | None = None,
):
# don't
set_var = set_arg or {1,2,3}
list_var = list_arg or ["a", "b", "c"]
dict_var = dict_arg or {"a": 3}
Because if user inputs empty set, list, or dict they would be disregarded and the defaults used.
Example
Disclaimer: the example functions are far from sophisticated.
Assuming you have the following code (yielding no ruff
nor pyright
errors):
def is_in_set[T](x: T, set_: set[T]) -> bool:
return x in set_
def ith_elem[T](i: int, list_: list[T]) -> T:
return list_[i]
def at_key[T](key: str, dict_: dict[str, T]) -> T:
return dict_[key]
if __name__ == "__main__":
print(is_in_set(1, {1, 2, 3}))
print(is_in_set(4, {1, 2, 3}))
print(ith_elem(0, ["a", "b", "c"]))
print(ith_elem(2, ["a", "b", "c"]))
print(at_key("a", {"a": 3}))
And want to add defaults to the set_
, list_
, and dict_
arguments.
If you do it like this:
def is_in_set[T](x: T, set_: set[T] = {1, 2, 3}) -> bool:
return x in set_
def ith_elem[T](i: int, list_: list[T] = ["a", "b", "c")]) -> T:
return list_[i]
def at_key[T](key: str, dict_: dict[str, T] = {"a": 3}) -> T:
return dict_[key]
if __name__ == "__main__":
print(is_in_set(1, {1, 2, 3}))
print(is_in_set(4, {1, 2, 3}))
print(is_in_set(1))
print(is_in_set(4))
print(ith_elem(0, ["a", "b", "c"]))
print(ith_elem(2, ["a", "b", "c"]))
print(ith_elem(0))
print(ith_elem(2))
print(at_key("a", {"a": 3}))
print(at_key("a"))
it will work and leave pyright
happy, but doing ruff check .
will yield
main.py:1:39: B006 Do not use mutable data structures for argument defaults
|
1 | def is_in_set[T](x: T, set_: set[T] = {1, 2, 3}) -> bool:
| ^^^^^^^^^ B006
2 | return x in set_
|
= help: Replace with `None`; initialize within function
main.py:5:42: B006 Do not use mutable data structures for argument defaults
|
5 | def ith_elem[T](i: int, list_: list[T] = ["a", "b", "c"]) -> T:
| ^^^^^^^^^^^^^^^ B006
6 | return list_[i]
|
= help: Replace with `None`; initialize within function
main.py:9:47: B006 Do not use mutable data structures for argument defaults
|
9 | def at_key[T](key: str, dict_: dict[str, T] = {"a": 3}) -> T:
| ^^^^^^^^ B006
10 | return dict_[key]
|
= help: Replace with `None`; initialize within function
Found 3 errors.
No fixes available (3 hidden fixes can be enabled with the `--unsafe-fixes` option).
Using immutable default arguments would lead to a code like this:
from types import MappingProxyType
def is_in_set[T](x: T, set_: set[T] = frozenset({1, 2, 3})) -> bool:
return x in set_
def ith_elem[T](i: int, list_: list[T] = ("a", "b", "c")) -> T:
return list_[i]
def at_key[T](key: str, dict_: dict[str, T] = MappingProxyType({"a": 3})) -> T:
return dict_[key]
if __name__ == "__main__":
print(is_in_set(1, {1, 2, 3}))
print(is_in_set(4, {1, 2, 3}))
print(is_in_set(1))
print(is_in_set(4))
print(ith_elem(0, ["a", "b", "c"]))
print(ith_elem(2, ["a", "b", "c"]))
print(ith_elem(0))
print(ith_elem(2))
print(at_key("a", {"a": 3}))
print(at_key("a"))
But it will lead to pyright
errors:
/home/artur/Projects/tmp/defaults/main.py
/home/artur/Projects/tmp/defaults/main.py:1:39 - error: Expression of type "frozenset[int]" cannot be assigned to parameter of type "set[T@is_in_set]"
"frozenset[int]" is not assignable to "set[T@is_in_set]" (reportArgumentType)
/home/artur/Projects/tmp/defaults/main.py:5:42 - error: Expression of type "tuple[Literal['a'], Literal['b'], Literal['c']]" cannot be assigned to parameter of type "list[T@ith_elem]"
"tuple[Literal['a'], Literal['b'], Literal['c']]" is not assignable to "list[T@ith_elem]" (reportArgumentType)
/home/artur/Projects/tmp/defaults/main.py:9:47 - error: "MappingProxyType" is not defined (reportUndefinedVariable)
/home/artur/Projects/tmp/defaults/main.py:16:11 - error: Argument of type "frozenset[int]" cannot be assigned to parameter "set_" of type "set[T@is_in_set]" in function "is_in_set"
"frozenset[int]" is not assignable to "set[T@is_in_set]" (reportArgumentType)
/home/artur/Projects/tmp/defaults/main.py:17:11 - error: Argument of type "frozenset[int]" cannot be assigned to parameter "set_" of type "set[T@is_in_set]" in function "is_in_set"
"frozenset[int]" is not assignable to "set[T@is_in_set]" (reportArgumentType)
/home/artur/Projects/tmp/defaults/main.py:20:11 - error: Argument of type "tuple[Literal['a'], Literal['b'], Literal['c']]" cannot be assigned to parameter "list_" of type "list[T@ith_elem]" in function "ith_elem"
"tuple[Literal['a'], Literal['b'], Literal['c']]" is not assignable to "list[T@ith_elem]" (reportArgumentType)
/home/artur/Projects/tmp/defaults/main.py:21:11 - error: Argument of type "tuple[Literal['a'], Literal['b'], Literal['c']]" cannot be assigned to parameter "list_" of type "list[T@ith_elem]" in function "ith_elem"
"tuple[Literal['a'], Literal['b'], Literal['c']]" is not assignable to "list[T@ith_elem]" (reportArgumentType)
7 errors, 0 warnings, 0 informations
Adjusting the types int he last example to be less strict will lead to the following code:
from collections.abc import Mapping, Sequence, Set
from types import MappingProxyType
def is_in_set[T](x: T, set_: Set[T] = frozenset({1, 2, 3})) -> bool:
return x in set_
def ith_elem[T](i: int, list_: Sequence[T] = ("a", "b", "c")) -> T:
return list_[i]
def at_key[T](key: str, dict_: Mapping[str, T] = MappingProxyType({"a": 3})) -> T:
return dict_[key]
if __name__ == "__main__":
print(is_in_set(1, {1, 2, 3}))
print(is_in_set(4, {1, 2, 3}))
print(is_in_set(1))
print(is_in_set(4))
print(ith_elem(0, ["a", "b", "c"]))
print(ith_elem(2, ["a", "b", "c"]))
print(ith_elem(0))
print(ith_elem(2))
print(at_key("a", {"a": 3}))
print(at_key("a"))
which works and makes both ruff
and pyright
succeed.
Note that since Python 3.9
you should import Mapping, Sequence, Set
and a myriad of other types from collections.abc
and not typing
.
https://docs.python.org/3/library/typing.html#typing.Mapping
https://docs.python.org/3/library/typing.html#typing.Sequence