Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Allow typeddict mismatches in non-exhaustive contexts
  • Loading branch information
sterliakov committed Aug 4, 2025
commit 1cfb13039a183ccb76292cc196133d73112f3b67
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
14 changes: 14 additions & 0 deletions test-data/unit/check-typeddict.test
Original file line number Diff line number Diff line change
Expand Up @@ -4289,3 +4289,17 @@ 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"}

def accepts_td(d: Union[TD, dict[str, str]]) -> None: ...

accepts_td({"x": "foo"})
[builtins fixtures/dict.pyi]
[typing fixtures/typing-typeddict.pyi]