Skip to content
Open
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
21 changes: 13 additions & 8 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -814,7 +814,7 @@ def validate_typeddict_kwargs(
self, kwargs: Iterable[tuple[Expression | None, Expression]], callee: TypedDictType
) -> tuple[dict[str, list[Expression]], set[str]] | None:
# All (actual or mapped from ** unpacks) expressions that can match given key.
result = defaultdict(list)
result: defaultdict[str, list[Expression]] = defaultdict(list)
# Keys that are guaranteed to be present no matter what (e.g. for all items of a union)
always_present_keys = set()
# Indicates latest encountered ** unpack of a non-closed type among items.
Expand All @@ -837,9 +837,12 @@ def validate_typeddict_kwargs(
return None
else:
# A directly present key unconditionally shadows all previously found
# values from ** items.
# TODO: for duplicate keys, type-check all values.
result[literal_value] = [item_arg]
# values from ** items. Preserve directly present values, since their
# expressions are still evaluated even when a later value shadows them.
previous_values = [
value for value in result[literal_value] if not isinstance(value, TempNode)
]
result[literal_value] = [item_arg, *previous_values]
always_present_keys.add(literal_value)
else:
is_valid, is_open = self.validate_star_typeddict_item(
Expand Down Expand Up @@ -917,11 +920,13 @@ def validate_star_typeddict_item(
# some `overrides` types are narrower that types in `defaults`, and
# former are too wide for `Config`.
if result[key]:
first = result[key][0]
if not isinstance(first, TempNode):
explicit_values = [
value for value in result[key] if not isinstance(value, TempNode)
]
if explicit_values:
# We must always preserve any non-synthetic values, so that
# we will accept them even if they are shadowed.
result[key] = [first, arg]
# we type-check them even if they are shadowed.
result[key] = [*explicit_values, arg]
else:
result[key] = [arg]
else:
Expand Down
20 changes: 20 additions & 0 deletions test-data/unit/check-typeddict.test
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,26 @@ p = Point(x='meaning_of_life', y=1337) # E: Incompatible types (expression has
[builtins fixtures/dict.pyi]
[typing fixtures/typing-typeddict.pyi]

[case testTypedDictDuplicateKeysTypeCheckAllValues]
from typing import TypedDict

class Payload(TypedDict):
value: int

def takes_str(value: str) -> int:
return 0

constructed = Payload({"value": takes_str(1), "value": 0}) # E: Argument 1 to "takes_str" has incompatible type "int"; expected "str"
contextual: Payload = {"value": takes_str(1), "value": 0} # E: Argument 1 to "takes_str" has incompatible type "int"; expected "str"
overrides: Payload = {"value": 0}
with_unpack = Payload({
"value": takes_str(1), # E: Argument 1 to "takes_str" has incompatible type "int"; expected "str"
"value": takes_str(2), # E: Argument 1 to "takes_str" has incompatible type "int"; expected "str"
**overrides,
})
[builtins fixtures/dict.pyi]
[typing fixtures/typing-typeddict.pyi]

[case testCannotCreateTypedDictInstanceWithInlineTypedDict]
from typing import TypedDict
D = TypedDict('D', {
Expand Down
Loading