@@ -850,8 +850,8 @@ def validate_typeddict_kwargs(
850850 result = defaultdict (list )
851851 # Keys that are guaranteed to be present no matter what (e.g. for all items of a union)
852852 always_present_keys = set ()
853- # Indicates latest encountered ** unpack among items.
854- last_star_found = None
853+ # Indicates latest encountered ** unpack of a non-closed type among items.
854+ last_open_star_found = None
855855
856856 for item_name_expr , item_arg in kwargs :
857857 if item_name_expr :
@@ -875,22 +875,30 @@ def validate_typeddict_kwargs(
875875 result [literal_value ] = [item_arg ]
876876 always_present_keys .add (literal_value )
877877 else :
878- last_star_found = item_arg
879- if not self .validate_star_typeddict_item (
878+ is_valid , is_open = self .validate_star_typeddict_item (
880879 item_arg , callee , result , always_present_keys
881- ):
880+ )
881+ if not is_valid :
882882 return None
883- if self .chk .options .extra_checks and last_star_found is not None :
883+ if is_open :
884+ last_open_star_found = item_arg
885+ if self .chk .options .extra_checks and last_open_star_found is not None :
886+ if callee .is_closed :
887+ self .chk .fail (
888+ "Cannot unpack item that may contain extra keys into a closed TypedDict" ,
889+ last_open_star_found ,
890+ code = codes .TYPEDDICT_ITEM ,
891+ )
884892 absent_keys = []
885893 for key in callee .items :
886894 if key not in callee .required_keys and key not in result :
887895 absent_keys .append (key )
888896 if absent_keys :
889- # Having an optional key not explicitly declared by a ** unpacked
897+ # Having an optional key not explicitly declared by a ** unpacked open
890898 # TypedDict is unsafe, it may be an (incompatible) subtype at runtime.
891899 # TODO: catch the cases where a declared key is overridden by a subsequent
892900 # ** item without it (and not again overridden with complete ** item).
893- self .msg .non_required_keys_absent_with_star (absent_keys , last_star_found )
901+ self .msg .non_required_keys_absent_with_star (absent_keys , last_open_star_found )
894902 return result , always_present_keys
895903
896904 def validate_star_typeddict_item (
@@ -899,14 +907,18 @@ def validate_star_typeddict_item(
899907 callee : TypedDictType ,
900908 result : dict [str , list [Expression ]],
901909 always_present_keys : set [str ],
902- ) -> bool :
910+ ) -> tuple [ bool , bool ] :
903911 """Update keys/expressions from a ** expression in TypedDict constructor.
904912
905- Note `result` and `always_present_keys` are updated in place. Return true if the
906- expression `item_arg` may valid in `callee` TypedDict context.
913+ Note `result` and `always_present_keys` are updated in place.
914+
915+ First tuple item returned is true if the expression `item_arg` may valid
916+ in `callee` TypedDict context. Second tuple item returned is true if the
917+ expression may contain other keys not explicitly declared.
907918 """
908919 inferred = get_proper_type (self .accept (item_arg , type_context = callee ))
909- possible_tds = []
920+ any_fallback = False
921+ possible_tds : list [TypedDictType ] = []
910922 if isinstance (inferred , TypedDictType ):
911923 possible_tds = [inferred ]
912924 elif isinstance (inferred , UnionType ):
@@ -915,10 +927,14 @@ def validate_star_typeddict_item(
915927 possible_tds .append (item )
916928 elif not self .valid_unpack_fallback_item (item ):
917929 self .msg .unsupported_target_for_star_typeddict (item , item_arg )
918- return False
930+ return False , True
931+ else :
932+ any_fallback = True
919933 elif not self .valid_unpack_fallback_item (inferred ):
920934 self .msg .unsupported_target_for_star_typeddict (inferred , item_arg )
921- return False
935+ return False , True
936+ else :
937+ any_fallback = True
922938 all_keys : set [str ] = set ()
923939 for td in possible_tds :
924940 all_keys |= td .items .keys ()
@@ -947,7 +963,8 @@ def validate_star_typeddict_item(
947963 # If this key is not required at least in some item of a union
948964 # it may not shadow previous item, so we need to type check both.
949965 result [key ].append (arg )
950- return True
966+ all_closed = all (t .is_closed for t in possible_tds )
967+ return True , any_fallback or not all_closed
951968
952969 def valid_unpack_fallback_item (self , typ : ProperType ) -> bool :
953970 if isinstance (typ , AnyType ):
0 commit comments