@@ -820,8 +820,8 @@ def validate_typeddict_kwargs(
820820 result = defaultdict (list )
821821 # Keys that are guaranteed to be present no matter what (e.g. for all items of a union)
822822 always_present_keys = set ()
823- # Indicates latest encountered ** unpack among items.
824- last_star_found = None
823+ # Indicates latest encountered ** unpack of a non-closed type among items.
824+ last_open_star_found = None
825825
826826 for item_name_expr , item_arg in kwargs :
827827 if item_name_expr :
@@ -845,22 +845,30 @@ def validate_typeddict_kwargs(
845845 result [literal_value ] = [item_arg ]
846846 always_present_keys .add (literal_value )
847847 else :
848- last_star_found = item_arg
849- if not self .validate_star_typeddict_item (
848+ is_valid , is_open = self .validate_star_typeddict_item (
850849 item_arg , callee , result , always_present_keys
851- ):
850+ )
851+ if not is_valid :
852852 return None
853- if self .chk .options .extra_checks and last_star_found is not None :
853+ if is_open :
854+ last_open_star_found = item_arg
855+ if self .chk .options .extra_checks and last_open_star_found is not None :
856+ if callee .is_closed :
857+ self .chk .fail (
858+ "Cannot unpack item that may contain extra keys into a closed TypedDict" ,
859+ last_open_star_found ,
860+ code = codes .TYPEDDICT_ITEM ,
861+ )
854862 absent_keys = []
855863 for key in callee .items :
856864 if key not in callee .required_keys and key not in result :
857865 absent_keys .append (key )
858866 if absent_keys :
859- # Having an optional key not explicitly declared by a ** unpacked
867+ # Having an optional key not explicitly declared by a ** unpacked open
860868 # TypedDict is unsafe, it may be an (incompatible) subtype at runtime.
861869 # TODO: catch the cases where a declared key is overridden by a subsequent
862870 # ** item without it (and not again overridden with complete ** item).
863- self .msg .non_required_keys_absent_with_star (absent_keys , last_star_found )
871+ self .msg .non_required_keys_absent_with_star (absent_keys , last_open_star_found )
864872 return result , always_present_keys
865873
866874 def validate_star_typeddict_item (
@@ -869,14 +877,18 @@ def validate_star_typeddict_item(
869877 callee : TypedDictType ,
870878 result : dict [str , list [Expression ]],
871879 always_present_keys : set [str ],
872- ) -> bool :
880+ ) -> tuple [ bool , bool ] :
873881 """Update keys/expressions from a ** expression in TypedDict constructor.
874882
875- Note `result` and `always_present_keys` are updated in place. Return true if the
876- expression `item_arg` may valid in `callee` TypedDict context.
883+ Note `result` and `always_present_keys` are updated in place.
884+
885+ First tuple item returned is true if the expression `item_arg` may valid
886+ in `callee` TypedDict context. Second tuple item returned is true if the
887+ expression may contain other keys not explicitly declared.
877888 """
878889 inferred = get_proper_type (self .accept (item_arg , type_context = callee ))
879- possible_tds = []
890+ any_fallback = False
891+ possible_tds : list [TypedDictType ] = []
880892 if isinstance (inferred , TypedDictType ):
881893 possible_tds = [inferred ]
882894 elif isinstance (inferred , UnionType ):
@@ -885,10 +897,14 @@ def validate_star_typeddict_item(
885897 possible_tds .append (item )
886898 elif not self .valid_unpack_fallback_item (item ):
887899 self .msg .unsupported_target_for_star_typeddict (item , item_arg )
888- return False
900+ return False , True
901+ else :
902+ any_fallback = True
889903 elif not self .valid_unpack_fallback_item (inferred ):
890904 self .msg .unsupported_target_for_star_typeddict (inferred , item_arg )
891- return False
905+ return False , True
906+ else :
907+ any_fallback = True
892908 all_keys : set [str ] = set ()
893909 for td in possible_tds :
894910 all_keys |= td .items .keys ()
@@ -917,7 +933,8 @@ def validate_star_typeddict_item(
917933 # If this key is not required at least in some item of a union
918934 # it may not shadow previous item, so we need to type check both.
919935 result [key ].append (arg )
920- return True
936+ all_closed = all (t .is_closed for t in possible_tds )
937+ return True , any_fallback or not all_closed
921938
922939 def valid_unpack_fallback_item (self , typ : ProperType ) -> bool :
923940 if isinstance (typ , AnyType ):
0 commit comments