From 17c709fcac7a805e7770102312f0b851a36d6be4 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Wed, 4 Jun 2025 04:20:42 +0200 Subject: [PATCH 1/4] Exclude dict constraints when a conflicting TypedDict constraint exists --- mypy/constraints.py | 18 +++++++++++++++++- test-data/unit/check-inference.test | 24 ++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index 293618556203..62113b6ac58e 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -273,7 +273,23 @@ def infer_constraints_for_callable( if any(isinstance(v, ParamSpecType) for v in callee.variables): # As a perf optimization filter imprecise constraints only when we can have them. constraints = filter_imprecise_kinds(constraints) - return constraints + + return _omit_dict_constraints_if_typeddict_found(constraints) + + +def _omit_dict_constraints_if_typeddict_found(constraints: list[Constraint]) -> list[Constraint]: + cmap = {} + for con in constraints: + cmap.setdefault(con.type_var, []).append(con) + res = [] + for group in cmap.values(): + if not any(isinstance(c.target, TypedDictType) for c in group): + res.extend(group) + continue + for c in group: + if not isinstance(c.target, Instance) or c.target.type.fullname != "builtins.dict": + res.append(c) + return res def infer_constraints( diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index b563eef0f8aa..b1ebb777d906 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -4149,3 +4149,27 @@ class Foo: else: self.qux = {} # E: Need type annotation for "qux" (hint: "qux: dict[, ] = ...") [builtins fixtures/dict.pyi] + +[case testInferLiteralTypedDict] +from typing import Generic, TypeVar, TypedDict + +T = TypeVar("T") + +class D(TypedDict): + x: int + +class A(Generic[T]): ... + + +def f(a: A[T], t: T) -> T: ... +def g(a: T, t: A[T]) -> T: ... + +def check(obj: A[D]) -> None: + reveal_type(f(obj, {"x": 1})) # N: Revealed type is "TypedDict('__main__.D', {'x': builtins.int})" + reveal_type(f(obj, {"x": ''})) # N: Revealed type is "TypedDict('__main__.D', {'x': builtins.int})" \ + # E: Incompatible types (expression has type "str", TypedDict item "x" has type "int") + reveal_type(g({"x": 1}, obj)) # N: Revealed type is "TypedDict('__main__.D', {'x': builtins.int})" + reveal_type(g({"x": ''}, obj)) # N: Revealed type is "TypedDict('__main__.D', {'x': builtins.int})" \ + # E: Incompatible types (expression has type "str", TypedDict item "x" has type "int") +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] From a0256246dae560d0e3f6338a89fd6e8c3c943767 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Wed, 4 Jun 2025 04:22:02 +0200 Subject: [PATCH 2/4] Only do that if we have any typeddicts --- mypy/constraints.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index 62113b6ac58e..7ba9edb5bc50 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -274,7 +274,9 @@ def infer_constraints_for_callable( # As a perf optimization filter imprecise constraints only when we can have them. constraints = filter_imprecise_kinds(constraints) - return _omit_dict_constraints_if_typeddict_found(constraints) + if any(isinstance(c.target, TypedDictType) for c in constraints): + constraints = _omit_dict_constraints_if_typeddict_found(constraints) + return constraints def _omit_dict_constraints_if_typeddict_found(constraints: list[Constraint]) -> list[Constraint]: From 0fca77eacaa811942a5e60fccfc8996c5703da40 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Wed, 4 Jun 2025 04:24:58 +0200 Subject: [PATCH 3/4] Fix own typing --- mypy/constraints.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index 7ba9edb5bc50..269fd68ad18f 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -274,22 +274,26 @@ def infer_constraints_for_callable( # As a perf optimization filter imprecise constraints only when we can have them. constraints = filter_imprecise_kinds(constraints) - if any(isinstance(c.target, TypedDictType) for c in constraints): + if any(isinstance(get_proper_type(c.target), TypedDictType) for c in constraints): constraints = _omit_dict_constraints_if_typeddict_found(constraints) return constraints def _omit_dict_constraints_if_typeddict_found(constraints: list[Constraint]) -> list[Constraint]: - cmap = {} + cmap: dict[TypeVarId, list[Constraint]] = {} for con in constraints: cmap.setdefault(con.type_var, []).append(con) res = [] for group in cmap.values(): - if not any(isinstance(c.target, TypedDictType) for c in group): + if not any(isinstance(get_proper_type(c.target), TypedDictType) for c in group): res.extend(group) continue for c in group: - if not isinstance(c.target, Instance) or c.target.type.fullname != "builtins.dict": + proper_target = get_proper_type(c.target) + if ( + not isinstance(proper_target, Instance) + or proper_target.type.fullname != "builtins.dict" + ): res.append(c) return res From a9e5c3bde3f1eaf81a27803c9a9be4324c2ca0f6 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Wed, 4 Jun 2025 13:56:06 +0200 Subject: [PATCH 4/4] Doc --- mypy/constraints.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mypy/constraints.py b/mypy/constraints.py index 269fd68ad18f..5245a21826a9 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -280,6 +280,10 @@ def infer_constraints_for_callable( def _omit_dict_constraints_if_typeddict_found(constraints: list[Constraint]) -> list[Constraint]: + """If a type variable has any `TypedDict` constraints, exclude its `dict` constraints.""" + + # This allows inferring types generic in typeddicts when a literal *and* explicit type + # are present among arguments - see testInferLiteralTypedDict. cmap: dict[TypeVarId, list[Constraint]] = {} for con in constraints: cmap.setdefault(con.type_var, []).append(con)