Skip to content

Fix dict assignment to a wider context containing an incompatible typeddict of the same shape #19592

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 16 additions & 8 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -5350,9 +5350,9 @@ def visit_dict_expr(self, e: DictExpr) -> Type:
# an error, but returns the TypedDict type that matches the literal it found
# that would cause a second error when that TypedDict type is returned upstream
# to avoid the second error, we always return TypedDict type that was requested
typeddict_contexts = self.find_typeddict_context(self.type_context[-1], e)
typeddict_contexts, exhaustive = self.find_typeddict_context(self.type_context[-1], e)
if typeddict_contexts:
if len(typeddict_contexts) == 1:
if len(typeddict_contexts) == 1 and exhaustive:
return self.check_typeddict_literal_in_context(e, typeddict_contexts[0])
# Multiple items union, check if at least one of them matches cleanly.
for typeddict_context in typeddict_contexts:
Expand All @@ -5363,7 +5363,8 @@ def visit_dict_expr(self, e: DictExpr) -> Type:
self.chk.store_types(tmap)
return ret_type
# No item matched without an error, so we can't unambiguously choose the item.
self.msg.typeddict_context_ambiguous(typeddict_contexts, e)
if exhaustive:
self.msg.typeddict_context_ambiguous(typeddict_contexts, e)

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

def find_typeddict_context(
self, context: Type | None, dict_expr: DictExpr
) -> list[TypedDictType]:
) -> tuple[list[TypedDictType], bool]:
"""Extract `TypedDict` members of the enclosing context.

Returns:
a 2-tuple, (found_candidates, is_exhaustive)
"""
context = get_proper_type(context)
if isinstance(context, TypedDictType):
return [context]
return [context], True
elif isinstance(context, UnionType):
items = []
exhaustive = True
for item in context.items:
item_contexts = self.find_typeddict_context(item, dict_expr)
item_contexts, item_exhaustive = self.find_typeddict_context(item, dict_expr)
for item_context in item_contexts:
if self.match_typeddict_call_with_dict(
item_context, dict_expr.items, dict_expr
):
items.append(item_context)
return items
exhaustive = exhaustive and item_exhaustive
return items, exhaustive
# No TypedDict type in context.
return []
return [], False

def visit_lambda_expr(self, e: LambdaExpr) -> Type:
"""Type check lambda expression."""
Expand Down
18 changes: 18 additions & 0 deletions test-data/unit/check-typeddict.test
Original file line number Diff line number Diff line change
Expand Up @@ -4289,3 +4289,21 @@ inputs: Sequence[Component] = [{
}]
[builtins fixtures/dict.pyi]
[typing fixtures/typing-typeddict.pyi]

[case testTypedDictAssignableToWiderContext]
from typing import TypedDict, Union

class TD(TypedDict):
x: int

x: Union[TD, dict[str, str]] = {"x": "foo"}
y: Union[TD, dict[str, int]] = {"x": "foo"} # E: Dict entry 0 has incompatible type "str": "str"; expected "str": "int"

def ok(d: Union[TD, dict[str, str]]) -> None: ...
ok({"x": "foo"})

def bad(d: Union[TD, dict[str, int]]) -> None: ...
bad({"x": "foo"}) # E: Dict entry 0 has incompatible type "str": "str"; expected "str": "int"

[builtins fixtures/dict.pyi]
[typing fixtures/typing-typeddict.pyi]
Loading