Skip to content

Commit 3c3065c

Browse files
authored
Improve error messages for unexpected keyword arguments in overloaded functions (#20592)
This PR improves error messages when calling overloaded functions with unexpected keyword arguments, making it easier to identify and fix typos.
1 parent 410f933 commit 3c3065c

3 files changed

Lines changed: 276 additions & 1 deletion

File tree

mypy/checkexpr.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2893,7 +2893,9 @@ def check_overload_call(
28932893
code = None
28942894
else:
28952895
code = codes.OPERATOR
2896-
self.msg.no_variant_matches_arguments(callee, arg_types, context, code=code)
2896+
self.msg.no_variant_matches_arguments(
2897+
callee, arg_types, context, arg_names=arg_names, arg_kinds=arg_kinds, code=code
2898+
)
28972899

28982900
result = self.check_call(
28992901
target,

mypy/messages.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,14 +1072,100 @@ def no_variant_matches_arguments(
10721072
arg_types: list[Type],
10731073
context: Context,
10741074
*,
1075+
arg_names: Sequence[str | None] | None,
1076+
arg_kinds: list[ArgKind] | None = None,
10751077
code: ErrorCode | None = None,
10761078
) -> None:
10771079
code = code or codes.CALL_OVERLOAD
10781080
name = callable_name(overload)
10791081
if name:
10801082
name_str = f" of {name}"
1083+
for_func = f" for overloaded function {name}"
10811084
else:
10821085
name_str = ""
1086+
for_func = ""
1087+
1088+
# For keyword argument errors
1089+
unexpected_kwargs: list[tuple[str, Type]] = []
1090+
if arg_names is not None and arg_kinds is not None:
1091+
all_valid_kwargs: set[str] = set()
1092+
for item in overload.items:
1093+
for i, arg_name in enumerate(item.arg_names):
1094+
if arg_name is not None and item.arg_kinds[i] != ARG_STAR:
1095+
all_valid_kwargs.add(arg_name)
1096+
if item.is_kw_arg:
1097+
all_valid_kwargs.clear()
1098+
break
1099+
1100+
if all_valid_kwargs:
1101+
for i, (arg_name, arg_kind) in enumerate(zip(arg_names, arg_kinds)):
1102+
if arg_kind == ARG_NAMED and arg_name is not None:
1103+
if arg_name not in all_valid_kwargs:
1104+
unexpected_kwargs.append((arg_name, arg_types[i]))
1105+
1106+
if unexpected_kwargs:
1107+
all_kwargs_confident = True
1108+
kwargs_with_suggestions: list[tuple[str, list[str]]] = []
1109+
kwargs_without_suggestions: list[str] = []
1110+
1111+
# Find suggestions for each unexpected kwarg, prioritizing type-matching args
1112+
for kwarg_name, kwarg_type in unexpected_kwargs:
1113+
matching_type_args: list[str] = []
1114+
not_matching_type_args: list[str] = []
1115+
has_matching_variant = False
1116+
1117+
for item in overload.items:
1118+
item_has_type_match = False
1119+
for i, formal_type in enumerate(item.arg_types):
1120+
formal_name = item.arg_names[i]
1121+
if formal_name is not None and item.arg_kinds[i] != ARG_STAR:
1122+
if is_subtype(kwarg_type, formal_type):
1123+
if formal_name not in matching_type_args:
1124+
matching_type_args.append(formal_name)
1125+
item_has_type_match = True
1126+
elif formal_name not in not_matching_type_args:
1127+
not_matching_type_args.append(formal_name)
1128+
if item_has_type_match:
1129+
has_matching_variant = True
1130+
1131+
matches = best_matches(kwarg_name, matching_type_args, n=3)
1132+
if not matches:
1133+
matches = best_matches(kwarg_name, not_matching_type_args, n=3)
1134+
1135+
if matches:
1136+
kwargs_with_suggestions.append((kwarg_name, matches))
1137+
else:
1138+
kwargs_without_suggestions.append(kwarg_name)
1139+
1140+
if not has_matching_variant:
1141+
all_kwargs_confident = False
1142+
1143+
for kwarg_name, matches in kwargs_with_suggestions:
1144+
self.fail(
1145+
f'Unexpected keyword argument "{kwarg_name}"'
1146+
f"{for_func}; did you mean {pretty_seq(matches, 'or')}?",
1147+
context,
1148+
code=code,
1149+
)
1150+
1151+
if kwargs_without_suggestions:
1152+
if len(kwargs_without_suggestions) == 1:
1153+
self.fail(
1154+
f'Unexpected keyword argument "{kwargs_without_suggestions[0]}"{for_func}',
1155+
context,
1156+
code=code,
1157+
)
1158+
else:
1159+
quoted_names = ", ".join(f'"{n}"' for n in kwargs_without_suggestions)
1160+
self.fail(
1161+
f"Unexpected keyword arguments {quoted_names}{for_func}",
1162+
context,
1163+
code=code,
1164+
)
1165+
1166+
if all_kwargs_confident and len(unexpected_kwargs) == len(arg_types):
1167+
return
1168+
10831169
arg_types_str = ", ".join(format_type(arg, self.options) for arg in arg_types)
10841170
num_args = len(arg_types)
10851171
if num_args == 0:

test-data/unit/check-expressions.test

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2627,3 +2627,190 @@ def last_known_value() -> None:
26272627
x, y, z = xy # E: Unpacking a string is disallowed
26282628
reveal_type(z) # N: Revealed type is "builtins.str"
26292629
[builtins fixtures/primitives.pyi]
2630+
2631+
[case testOverloadUnexpectedKeywordArgWithTypoSuggestion]
2632+
from typing import overload, Union
2633+
2634+
@overload
2635+
def f(foobar: int) -> None: ...
2636+
2637+
@overload
2638+
def f(foobar: str) -> None: ...
2639+
2640+
def f(foobar: Union[int, str]) -> None: pass
2641+
2642+
f(fobar=1) # E: Unexpected keyword argument "fobar" for overloaded function "f"; did you mean "foobar"?
2643+
[builtins fixtures/list.pyi]
2644+
2645+
[case testOverloadUnexpectedKeywordArgNoMatch]
2646+
from typing import overload, Union
2647+
2648+
@overload
2649+
def f(foobar: int) -> None: ...
2650+
2651+
@overload
2652+
def f(foobar: str) -> None: ...
2653+
2654+
def f(foobar: Union[int, str]) -> None: pass
2655+
2656+
f(random=[1,2,3]) # E: Unexpected keyword argument "random" for overloaded function "f" \
2657+
# E: No overload variant of "f" matches argument type "list[int]" \
2658+
# N: Possible overload variants: \
2659+
# N: def f(foobar: int) -> None \
2660+
# N: def f(foobar: str) -> None
2661+
[builtins fixtures/list.pyi]
2662+
2663+
[case testOverloadMultipleUnexpectedKeywordArgs]
2664+
from typing import overload, Union
2665+
2666+
@overload
2667+
def f(foobar: int) -> None: ...
2668+
2669+
@overload
2670+
def f(foobar: str) -> None: ...
2671+
2672+
def f(foobar: Union[int, str]) -> None: pass
2673+
2674+
f(fobar=1, baz=2) # E: Unexpected keyword argument "fobar" for overloaded function "f"; did you mean "foobar"? \
2675+
# E: Unexpected keyword argument "baz" for overloaded function "f"
2676+
[builtins fixtures/list.pyi]
2677+
2678+
[case testOverloadManyUnexpectedKeywordArgs]
2679+
from typing import overload, Union
2680+
2681+
@overload
2682+
def f(foobar: int) -> None: ...
2683+
2684+
@overload
2685+
def f(foobar: str) -> None: ...
2686+
2687+
def f(foobar: Union[int, str]) -> None: pass
2688+
2689+
f(foobar=1, a=2, b=3, c=4, d=5, e=6) # E: Unexpected keyword arguments "a", "b", "c", "d", "e" for overloaded function "f" \
2690+
# E: No overload variant of "f" matches argument types "int", "int", "int", "int", "int", "int" \
2691+
# N: Possible overload variants: \
2692+
# N: def f(foobar: int) -> None \
2693+
# N: def f(foobar: str) -> None
2694+
[builtins fixtures/list.pyi]
2695+
2696+
[case testOverloadUnexpectedKeywordArgsWithTypeMismatch]
2697+
from typing import overload, Union
2698+
2699+
@overload
2700+
def f(foobar: int) -> None: ...
2701+
2702+
@overload
2703+
def f(foobar: str) -> None: ...
2704+
2705+
def f(foobar: Union[int, str]) -> None: pass
2706+
2707+
f(fobar=1, other=[1,2,3]) # E: Unexpected keyword argument "fobar" for overloaded function "f"; did you mean "foobar"? \
2708+
# E: Unexpected keyword argument "other" for overloaded function "f" \
2709+
# E: No overload variant of "f" matches argument types "int", "list[int]" \
2710+
# N: Possible overload variants: \
2711+
# N: def f(foobar: int) -> None \
2712+
# N: def f(foobar: str) -> None
2713+
[builtins fixtures/list.pyi]
2714+
2715+
[case testOverloadPositionalArgTypeMismatch]
2716+
from typing import overload, Union
2717+
2718+
@overload
2719+
def g(x: int, y: int) -> int: ...
2720+
2721+
@overload
2722+
def g(x: str, y: str) -> str: ...
2723+
2724+
def g(x: Union[int, str], y: Union[int, str]) -> Union[int, str]:
2725+
return x
2726+
2727+
g([1, 2], 3) # E: No overload variant of "g" matches argument types "list[int]", "int" \
2728+
# N: Possible overload variants: \
2729+
# N: def g(x: int, y: int) -> int \
2730+
# N: def g(x: str, y: str) -> str
2731+
[builtins fixtures/list.pyi]
2732+
2733+
[case testOverloadUnexpectedKeywordWithPositionalMismatch]
2734+
from typing import overload, Union
2735+
2736+
@overload
2737+
def g(x: int, y: int) -> int: ...
2738+
2739+
@overload
2740+
def g(x: str, y: str) -> str: ...
2741+
2742+
def g(x: Union[int, str], y: Union[int, str]) -> Union[int, str]:
2743+
return x
2744+
2745+
g([1, 2], z=3) # E: Unexpected keyword argument "z" for overloaded function "g" \
2746+
# E: No overload variant of "g" matches argument types "list[int]", "int" \
2747+
# N: Possible overload variants: \
2748+
# N: def g(x: int, y: int) -> int \
2749+
# N: def g(x: str, y: str) -> str
2750+
[builtins fixtures/list.pyi]
2751+
2752+
[case testOverloadNamedArgTypeMismatch]
2753+
from typing import overload, Union
2754+
2755+
@overload
2756+
def g(x: int, y: int) -> int: ...
2757+
2758+
@overload
2759+
def g(x: str, y: str) -> str: ...
2760+
2761+
def g(x: Union[int, str], y: Union[int, str]) -> Union[int, str]:
2762+
return x
2763+
2764+
g(x="hello", y=1) # E: No overload variant of "g" matches argument types "str", "int" \
2765+
# N: Possible overload variants: \
2766+
# N: def g(x: int, y: int) -> int \
2767+
# N: def g(x: str, y: str) -> str
2768+
[builtins fixtures/list.pyi]
2769+
2770+
[case testOverloadMixedArgsWithNonFirstKeywordTypo]
2771+
from typing import overload, Union
2772+
2773+
@overload
2774+
def f(x: int, name: int, value: int) -> None: ...
2775+
2776+
@overload
2777+
def f(x: str, name: str, value: str) -> None: ...
2778+
2779+
def f(x: Union[int, str], name: Union[int, str], value: Union[int, str]) -> None: pass
2780+
2781+
f(1, name=2, valeu=3) # E: Unexpected keyword argument "valeu" for overloaded function "f"; did you mean "value"? \
2782+
# E: No overload variant of "f" matches argument types "int", "int", "int" \
2783+
# N: Possible overload variants: \
2784+
# N: def f(x: int, name: int, value: int) -> None \
2785+
# N: def f(x: str, name: str, value: str) -> None
2786+
[builtins fixtures/list.pyi]
2787+
2788+
[case testOverloadTwoMisspelledKeywordsDifferentTargets]
2789+
from typing import overload, Union
2790+
2791+
@overload
2792+
def f(name: int, value: int) -> None: ...
2793+
2794+
@overload
2795+
def f(name: str, value: str) -> None: ...
2796+
2797+
def f(name: Union[int, str], value: Union[int, str]) -> None: pass
2798+
2799+
f(nme=1, valeu=2) # E: Unexpected keyword argument "nme" for overloaded function "f"; did you mean "name"? \
2800+
# E: Unexpected keyword argument "valeu" for overloaded function "f"; did you mean "value"?
2801+
[builtins fixtures/list.pyi]
2802+
2803+
[case testOverloadMethodKeywordTypo]
2804+
from typing import overload, Union
2805+
2806+
class A:
2807+
@overload
2808+
def f(self, foobar: int) -> None: ...
2809+
2810+
@overload
2811+
def f(self, foobar: str) -> None: ...
2812+
2813+
def f(self, foobar: Union[int, str]) -> None: pass
2814+
2815+
A().f(fobar=1) # E: Unexpected keyword argument "fobar" for overloaded function "f" of "A"; did you mean "foobar"?
2816+
[builtins fixtures/list.pyi]

0 commit comments

Comments
 (0)