Skip to content

Commit 3387d6f

Browse files
authored
Fix dict assignment to a wider context containing an incompatible typeddict of the same shape (#19592)
Fixes #19590. Fixes #14991 (oops, I forgot I have already reported this...). When a typeddict context does not cover all available options, proceed with checking as usual against the whole context if none of the items matches in full despite being structurally compatible.
1 parent 64dff42 commit 3387d6f

File tree

2 files changed

+34
-8
lines changed

2 files changed

+34
-8
lines changed

mypy/checkexpr.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5350,9 +5350,9 @@ def visit_dict_expr(self, e: DictExpr) -> Type:
53505350
# an error, but returns the TypedDict type that matches the literal it found
53515351
# that would cause a second error when that TypedDict type is returned upstream
53525352
# to avoid the second error, we always return TypedDict type that was requested
5353-
typeddict_contexts = self.find_typeddict_context(self.type_context[-1], e)
5353+
typeddict_contexts, exhaustive = self.find_typeddict_context(self.type_context[-1], e)
53545354
if typeddict_contexts:
5355-
if len(typeddict_contexts) == 1:
5355+
if len(typeddict_contexts) == 1 and exhaustive:
53565356
return self.check_typeddict_literal_in_context(e, typeddict_contexts[0])
53575357
# Multiple items union, check if at least one of them matches cleanly.
53585358
for typeddict_context in typeddict_contexts:
@@ -5363,7 +5363,8 @@ def visit_dict_expr(self, e: DictExpr) -> Type:
53635363
self.chk.store_types(tmap)
53645364
return ret_type
53655365
# No item matched without an error, so we can't unambiguously choose the item.
5366-
self.msg.typeddict_context_ambiguous(typeddict_contexts, e)
5366+
if exhaustive:
5367+
self.msg.typeddict_context_ambiguous(typeddict_contexts, e)
53675368

53685369
# fast path attempt
53695370
dt = self.fast_dict_type(e)
@@ -5425,22 +5426,29 @@ def visit_dict_expr(self, e: DictExpr) -> Type:
54255426

54265427
def find_typeddict_context(
54275428
self, context: Type | None, dict_expr: DictExpr
5428-
) -> list[TypedDictType]:
5429+
) -> tuple[list[TypedDictType], bool]:
5430+
"""Extract `TypedDict` members of the enclosing context.
5431+
5432+
Returns:
5433+
a 2-tuple, (found_candidates, is_exhaustive)
5434+
"""
54295435
context = get_proper_type(context)
54305436
if isinstance(context, TypedDictType):
5431-
return [context]
5437+
return [context], True
54325438
elif isinstance(context, UnionType):
54335439
items = []
5440+
exhaustive = True
54345441
for item in context.items:
5435-
item_contexts = self.find_typeddict_context(item, dict_expr)
5442+
item_contexts, item_exhaustive = self.find_typeddict_context(item, dict_expr)
54365443
for item_context in item_contexts:
54375444
if self.match_typeddict_call_with_dict(
54385445
item_context, dict_expr.items, dict_expr
54395446
):
54405447
items.append(item_context)
5441-
return items
5448+
exhaustive = exhaustive and item_exhaustive
5449+
return items, exhaustive
54425450
# No TypedDict type in context.
5443-
return []
5451+
return [], False
54445452

54455453
def visit_lambda_expr(self, e: LambdaExpr) -> Type:
54465454
"""Type check lambda expression."""

test-data/unit/check-typeddict.test

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4289,3 +4289,21 @@ inputs: Sequence[Component] = [{
42894289
}]
42904290
[builtins fixtures/dict.pyi]
42914291
[typing fixtures/typing-typeddict.pyi]
4292+
4293+
[case testTypedDictAssignableToWiderContext]
4294+
from typing import TypedDict, Union
4295+
4296+
class TD(TypedDict):
4297+
x: int
4298+
4299+
x: Union[TD, dict[str, str]] = {"x": "foo"}
4300+
y: Union[TD, dict[str, int]] = {"x": "foo"} # E: Dict entry 0 has incompatible type "str": "str"; expected "str": "int"
4301+
4302+
def ok(d: Union[TD, dict[str, str]]) -> None: ...
4303+
ok({"x": "foo"})
4304+
4305+
def bad(d: Union[TD, dict[str, int]]) -> None: ...
4306+
bad({"x": "foo"}) # E: Dict entry 0 has incompatible type "str": "str"; expected "str": "int"
4307+
4308+
[builtins fixtures/dict.pyi]
4309+
[typing fixtures/typing-typeddict.pyi]

0 commit comments

Comments
 (0)