Why is the type of an unpacked dict not considered for overload matching? #5231
|
I am trying to define an overloaded function signature which looks something like this: from typing import Any, Dict, Optional, Union, overload
from typing_extensions import Literal
@overload
def inverse(
val: float = ...,
*,
raise_error: Literal[True] = ...,
) -> float:
...
@overload
def inverse(
val: float = ...,
*,
raise_error: bool,
) -> Optional[float]:
...
def inverse(
val: float = 1.0,
*,
raise_error: bool = True,
) -> Optional[float]:
try:
return 1 / val
except ZeroDivisionError as e:
if raise_error:
raise ValueError("Cannot divide by zero") from e
return None
def get_a_bool() -> bool:
return True
# mypy and pyright agree:
reveal_type(inverse()) # Type of "inverse()" is "float"
reveal_type(inverse(raise_error=True)) # Type of "inverse(raise_error=True)" is "float"
reveal_type(inverse(raise_error=False)) # Type of "inverse(raise_error=False)" is "float | None"
reveal_type(inverse(raise_error=get_a_bool())) # Type of "inverse(raise_error=get_a_bool())" is "float | None"
kwargs_literal_true: Dict[str, Literal[True]] = {}
reveal_type(inverse(**kwargs_literal_true)) # Type of "inverse(**kwargs_literal_true)" is "float"
kwargs_literal_true_union_something: Dict[str, Union[Literal[True], int]] = {}
reveal_type(inverse(**kwargs_literal_true_union_something)) # Type of "inverse(**kwargs_literal_true_union_something)" is "float"
##############################################################################################################
# pyright says all of these below are float
kwargs_any: Dict[str, Any] = {}
reveal_type(inverse(**kwargs_any)) # Revealed type is "Any"
# mypy says the rest of these are "Union[builtins.float, None]"
kwargs_literal_true_union_bool: Dict[str, Union[Literal[True], bool]] = {}
reveal_type(inverse(**kwargs_literal_true_union_bool))
kwargs_literal_false_empty: Dict[str, Literal[False]] = {}
reveal_type(inverse(**kwargs_literal_false_empty))
kwargs_literal_both_empty: Dict[str, Literal[True, False]] = {}
reveal_type(inverse(**kwargs_literal_both_empty))
kwargs_literal_both_true: Dict[str, Literal[True, False]] = {"raise_error": True}
reveal_type(inverse(**kwargs_literal_both_true))
kwargs_literal_both_false: Dict[str, Literal[True, False]] = {"raise_error": False}
reveal_type(inverse(**kwargs_literal_both_false))
kwargs_empty: Dict[str, bool] = {}
reveal_type(inverse(**kwargs_empty))
kwargs_bool_false: Dict[str, bool] = {"raise_error": False}
reveal_type(inverse(**kwargs_bool_false))
kwargs_bool_true: Dict[str, bool] = {"raise_error": True}
reveal_type(inverse(**kwargs_bool_true))What I am trying say with this signature is that
and that
if we know types of the values of the pyright seems to be always assuming that the key used to match overload signatures not present, which leads to incorrectly inferred types. Could we even just return a union of all the possibly matched signatures irrespective of the |
Replies: 1 comment
|
You've uncovered a subtle difference in behavior between mypy and pyright that I hadn't previously noticed. It occurs in the case where you use an unpacked dict argument and the target call has keyword parameters with default arguments. Pyright's behavior assumed that such parameters would be supplied their argument from the default, so it was OK if the unpacked dict type was incompatible. Mypy apparently assumes that the unpacked dict may contain entries that correspond to all otherwise-unmatched keyword parameters, so the supplied type within the dict must be compatible with these keyword parameters even if they have a default argument value. This affects the overload behavior because your first overload includes a keyword-only parameter ( I could make a case for both pyright's and mypy's behavior here. Mypy's is more strict but can result in some false positive errors; pyright is more lenient and avoids false positives at the cost of not detecting some bugs. I think mypy's behavior is defensible, and it's better for the behavior to match between mypy and pyright in this case. I've therefore updated pyright's logic to match mypy's in this case. This may create some churn for some pyright users who were relying on the more lenient behavior, but it's a one-time change. There is still one small difference between pyright and mypy in one of your test cases above: kwargs_any: Dict[str, Any] = {}
reveal_type(inverse(**kwargs_any)) # Mypy: Any, pyright: float | NoneBoth pyright and mypy detect cases where an |
You've uncovered a subtle difference in behavior between mypy and pyright that I hadn't previously noticed. It occurs in the case where you use an unpacked dict argument and the target call has keyword parameters with default arguments. Pyright's behavior assumed that such parameters would be supplied their argument from the default, so it was OK if the unpacked dict type was incompatible. Mypy apparently assumes that the unpacked dict may contain entries that correspond to all otherwise-unmatched keyword parameters, so the supplied type within the dict must be compatible with these keyword parameters even if they have a default argument value.
This affects the overload behavior because y…