diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 172d44555b94..b4abb6e0677f 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -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. @@ -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( @@ -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: diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index 01414ee2f457..609608b009de 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -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', {