From adf39e1a9fb837135c183f62bfe8b3730f9ef120 Mon Sep 17 00:00:00 2001 From: Yosof Badr <23705518+YosofBadr@users.noreply.github.com> Date: Thu, 16 Apr 2026 04:35:03 +0900 Subject: [PATCH] fix: improve error message for incompatible **kwargs When a function is called with **kwargs whose value type is incompatible with the expected parameter types, mypy now adds a note suggesting the user annotate the ** argument as "**kwargs: Any" or use a TypedDict type for more precise typing. This addresses the confusing error messages reported in #8874 where multiple errors like 'has incompatible type "**Dict[str, int]"; expected "bool"' gave no actionable guidance on how to fix the issue. Fixes #8874 --- mypy/messages.py | 5 +++ test-data/unit/check-columns.test | 3 +- test-data/unit/check-expressions.test | 3 +- test-data/unit/check-functions.test | 3 +- test-data/unit/check-functools.test | 6 ++-- test-data/unit/check-generic-subtyping.test | 1 + test-data/unit/check-kwargs.test | 35 ++++++++++++++----- .../unit/check-parameter-specification.test | 9 +++-- test-data/unit/check-varargs.test | 3 +- 9 files changed, 51 insertions(+), 17 deletions(-) diff --git a/mypy/messages.py b/mypy/messages.py index cfd1a12ea77c9..ab0974fc3f6b7 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -804,6 +804,11 @@ def incompatible_argument( if isinstance(arg_type, Instance) and isinstance(type, Instance): notes = append_invariance_notes(notes, arg_type, type) notes = append_numbers_notes(notes, arg_type, type) + if arg_kind == ARG_STAR2 and isinstance(arg_type, Instance): + notes.append( + "Consider annotating the ** argument as" + ' "**kwargs: Any" or using a TypedDict' + ) object_type = get_proper_type(object_type) if isinstance(object_type, TypedDictType): code = codes.TYPEDDICT_ITEM diff --git a/test-data/unit/check-columns.test b/test-data/unit/check-columns.test index dd8389c195583..c3b6782f73ab2 100644 --- a/test-data/unit/check-columns.test +++ b/test-data/unit/check-columns.test @@ -71,7 +71,8 @@ def g(**x: int) -> None: pass a = [''] f(*a) # E:4: Argument 1 to "f" has incompatible type "*list[str]"; expected "int" b = {'x': 'y'} -g(**b) # E:5: Argument 1 to "g" has incompatible type "**dict[str, str]"; expected "int" +g(**b) # E:5: Argument 1 to "g" has incompatible type "**dict[str, str]"; expected "int" \ + # N:5: Consider annotating the ** argument as "**kwargs: Any" or using a TypedDict [builtins fixtures/dict.pyi] [case testColumnsMultipleStatementsPerLine] diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index 77d034902dc62..d5d98a27f6430 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -1770,7 +1770,8 @@ kw2 = {'x': ''} d2 = dict(it, **kw2) d2() # E: "dict[str, object]" not callable -d3 = dict(it, **kw2) # type: Dict[str, int] # E: Argument 2 to "dict" has incompatible type "**dict[str, str]"; expected "int" +d3 = dict(it, **kw2) # type: Dict[str, int] # E: Argument 2 to "dict" has incompatible type "**dict[str, str]"; expected "int" \ + # N: Consider annotating the ** argument as "**kwargs: Any" or using a TypedDict [builtins fixtures/dict.pyi] [case testDictFromIterableAndStarStarArgs2] diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index 3c3c438c60a95..c5642e40d1126 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -3769,7 +3769,8 @@ def foo(x: P, y: P) -> None: ... args: list[object] foo(*args) # E: Argument 1 to "foo" has incompatible type "*list[object]"; expected "P" kwargs: dict[str, object] -foo(**kwargs) # E: Argument 1 to "foo" has incompatible type "**dict[str, object]"; expected "P" +foo(**kwargs) # E: Argument 1 to "foo" has incompatible type "**dict[str, object]"; expected "P" \ + # N: Consider annotating the ** argument as "**kwargs: Any" or using a TypedDict [builtins fixtures/dict.pyi] [case testNoImplicitReturnErrorOnDeferral_no_empty] diff --git a/test-data/unit/check-functools.test b/test-data/unit/check-functools.test index 77070d61a013c..0fd4b3df7eebb 100644 --- a/test-data/unit/check-functools.test +++ b/test-data/unit/check-functools.test @@ -213,8 +213,10 @@ functools.partial(foo, 1, "a", "b", "c", d="a") # E: Argument 3 to "foo" has in def bar(*a: bytes, **k: int): p1("a", 2, 3, 4, d="a", **k) p1("a", d="a", **k) - p1("a", **k) # E: Argument 2 to "foo" has incompatible type "**dict[str, int]"; expected "str" - p1(**k) # E: Argument 1 to "foo" has incompatible type "**dict[str, int]"; expected "str" + p1("a", **k) # E: Argument 2 to "foo" has incompatible type "**dict[str, int]"; expected "str" \ + # N: Consider annotating the ** argument as "**kwargs: Any" or using a TypedDict + p1(**k) # E: Argument 1 to "foo" has incompatible type "**dict[str, int]"; expected "str" \ + # N: Consider annotating the ** argument as "**kwargs: Any" or using a TypedDict p1(*a) # E: Expected iterable as variadic argument diff --git a/test-data/unit/check-generic-subtyping.test b/test-data/unit/check-generic-subtyping.test index 7e954a6a66859..a9ef1f3cc91e5 100644 --- a/test-data/unit/check-generic-subtyping.test +++ b/test-data/unit/check-generic-subtyping.test @@ -1016,6 +1016,7 @@ main:12: note: Revealed type is "typing.Iterator[builtins.int]" main:13: note: Revealed type is "builtins.dict[builtins.int, builtins.str]" main:14: error: Argument after ** must have string keys main:14: error: Argument 1 to "func_with_kwargs" has incompatible type "**X1[str, int]"; expected "int" +main:14: note: Consider annotating the ** argument as "**kwargs: Any" or using a TypedDict [builtins fixtures/dict.pyi] [typing fixtures/typing-medium.pyi] diff --git a/test-data/unit/check-kwargs.test b/test-data/unit/check-kwargs.test index f11c2b6f4fc40..898a0d75f0ced 100644 --- a/test-data/unit/check-kwargs.test +++ b/test-data/unit/check-kwargs.test @@ -301,9 +301,20 @@ d: Dict[str, A] f(**d) f(x=A(), **d) d2: Dict[str, B] -f(**d2) # E: Argument 1 to "f" has incompatible type "**dict[str, B]"; expected "A" -f(x=A(), **d2) # E: Argument 2 to "f" has incompatible type "**dict[str, B]"; expected "A" -f(**{'x': B()}) # E: Argument 1 to "f" has incompatible type "**dict[str, B]"; expected "A" +f(**d2) # E: Argument 1 to "f" has incompatible type "**dict[str, B]"; expected "A" \ + # N: Consider annotating the ** argument as "**kwargs: Any" or using a TypedDict +f(x=A(), **d2) # E: Argument 2 to "f" has incompatible type "**dict[str, B]"; expected "A" \ + # N: Consider annotating the ** argument as "**kwargs: Any" or using a TypedDict +f(**{'x': B()}) # E: Argument 1 to "f" has incompatible type "**dict[str, B]"; expected "A" \ + # N: Consider annotating the ** argument as "**kwargs: Any" or using a TypedDict +[builtins fixtures/dict.pyi] + +[case testIncompatibleKwargsNote] +from typing import Any +def f(x: int, y: str) -> None: pass +d: dict[str, int] = {} +f(**d) # E: Argument 1 to "f" has incompatible type "**dict[str, int]"; expected "str" \ + # N: Consider annotating the ** argument as "**kwargs: Any" or using a TypedDict [builtins fixtures/dict.pyi] [case testKwargsAllowedInDunderCall] @@ -345,7 +356,8 @@ from typing import Mapping class MappingSubclass(Mapping[str, str]): pass def f(**kwargs: 'A') -> None: pass d: MappingSubclass -f(**d) # E: Argument 1 to "f" has incompatible type "**MappingSubclass"; expected "A" +f(**d) # E: Argument 1 to "f" has incompatible type "**MappingSubclass"; expected "A" \ + # N: Consider annotating the ** argument as "**kwargs: Any" or using a TypedDict class A: pass [builtins fixtures/dict.pyi] @@ -372,7 +384,8 @@ def f(a: 'A', b: 'B') -> None: pass d: Dict[str, Any] f(**d) d2: Dict[str, A] -f(**d2) # E: Argument 1 to "f" has incompatible type "**dict[str, A]"; expected "B" +f(**d2) # E: Argument 1 to "f" has incompatible type "**dict[str, A]"; expected "B" \ + # N: Consider annotating the ** argument as "**kwargs: Any" or using a TypedDict class A: pass class B: pass [builtins fixtures/dict.pyi] @@ -449,7 +462,8 @@ f(**a) # okay b = {'': ''} f(b) # E: Argument 1 to "f" has incompatible type "dict[str, str]"; expected "int" -f(**b) # E: Argument 1 to "f" has incompatible type "**dict[str, str]"; expected "int" +f(**b) # E: Argument 1 to "f" has incompatible type "**dict[str, str]"; expected "int" \ + # N: Consider annotating the ** argument as "**kwargs: Any" or using a TypedDict c = {0: 0} f(**c) # E: Argument after ** must have string keys @@ -506,11 +520,13 @@ def g(arg: int = 0, **kwargs: object) -> None: d = {} # type: Dict[str, object] f(**d) -g(**d) # E: Argument 1 to "g" has incompatible type "**dict[str, object]"; expected "int" +g(**d) # E: Argument 1 to "g" has incompatible type "**dict[str, object]"; expected "int" \ + # N: Consider annotating the ** argument as "**kwargs: Any" or using a TypedDict m = {} # type: Mapping[str, object] f(**m) -g(**m) # E: Argument 1 to "g" has incompatible type "**Mapping[str, object]"; expected "int" +g(**m) # E: Argument 1 to "g" has incompatible type "**Mapping[str, object]"; expected "int" \ + # N: Consider annotating the ** argument as "**kwargs: Any" or using a TypedDict [builtins fixtures/dict.pyi] [case testPassingEmptyDictWithStars] @@ -577,10 +593,13 @@ foo(**good2) foo(**good3) [out] main:36: error: Argument 1 to "foo" has incompatible type "**A[str, str]"; expected "float" +main:36: note: Consider annotating the ** argument as "**kwargs: Any" or using a TypedDict main:37: error: Argument 1 to "foo" has incompatible type "**B[str, str]"; expected "float" +main:37: note: Consider annotating the ** argument as "**kwargs: Any" or using a TypedDict main:38: error: Argument after ** must be a mapping, not "C[str, float]" main:39: error: Argument after ** must be a mapping, not "D" main:41: error: Argument 1 to "foo" has incompatible type "**dict[str, str]"; expected "float" +main:41: note: Consider annotating the ** argument as "**kwargs: Any" or using a TypedDict [builtins fixtures/dict.pyi] [case testLiteralKwargs] diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index b0808105a3858..725a2f0922df2 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -1276,7 +1276,8 @@ def c3(f: Callable[P, int], *args, **kwargs) -> int: ... def c4(f: Callable[P, int], *args: int, **kwargs: str) -> int: # but not ok to call: f(*args, **kwargs) # E: Argument 1 has incompatible type "*tuple[int, ...]"; expected "P.args" \ - # E: Argument 2 has incompatible type "**dict[str, str]"; expected "P.kwargs" + # E: Argument 2 has incompatible type "**dict[str, str]"; expected "P.kwargs" \ + # N: Consider annotating the ** argument as "**kwargs: Any" or using a TypedDict return 1 def f1(f: Callable[P, int], *args, **kwargs: P.kwargs) -> int: ... # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs" @@ -1306,7 +1307,8 @@ def c3(f: Callable[Concatenate[int, P], int], *args, **kwargs) -> int: ... def c4(f: Callable[Concatenate[int, P], int], *args: int, **kwargs: str) -> int: # but not ok to call: f(1, *args, **kwargs) # E: Argument 2 has incompatible type "*tuple[int, ...]"; expected "P.args" \ - # E: Argument 3 has incompatible type "**dict[str, str]"; expected "P.kwargs" + # E: Argument 3 has incompatible type "**dict[str, str]"; expected "P.kwargs" \ + # N: Consider annotating the ** argument as "**kwargs: Any" or using a TypedDict return 1 def f1(f: Callable[Concatenate[int, P], int], *args, **kwargs: P.kwargs) -> int: ... # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs" @@ -2417,7 +2419,8 @@ def run3(func: Callable[Concatenate[int, P], T], *args: P.args, **kwargs: P.kwar func2 = partial(func, 1, *args) d = {"":""} func2(**d) # E: Too few arguments \ - # E: Argument 1 has incompatible type "**dict[str, str]"; expected "P.kwargs" + # E: Argument 1 has incompatible type "**dict[str, str]"; expected "P.kwargs" \ + # N: Consider annotating the ** argument as "**kwargs: Any" or using a TypedDict return func2(**kwargs) def run4(func: Callable[Concatenate[int, P], T], *args: P.args, **kwargs: P.kwargs) -> T: diff --git a/test-data/unit/check-varargs.test b/test-data/unit/check-varargs.test index 172e57cf1a4b3..236a350735484 100644 --- a/test-data/unit/check-varargs.test +++ b/test-data/unit/check-varargs.test @@ -830,7 +830,8 @@ Weird = TypedDict("Weird", {"@": int}) def foo(**kwargs: Unpack[Weird]) -> None: reveal_type(kwargs["@"]) # N: Revealed type is "builtins.int" foo(**{"@": 42}) -foo(**{"no": "way"}) # E: Argument 1 to "foo" has incompatible type "**dict[str, str]"; expected "int" +foo(**{"no": "way"}) # E: Argument 1 to "foo" has incompatible type "**dict[str, str]"; expected "int" \ + # N: Consider annotating the ** argument as "**kwargs: Any" or using a TypedDict [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi]