@@ -3010,6 +3010,16 @@ func(v1=1, v2="optional", v3="ok")
30103010
30113011# error: [unknown-argument]
30123012func(v1 = 1 , v3 = " ok" , v4 = 1 )
3013+
3014+ td2 = TD2(v1 = 1 , v3 = " ok" )
3015+ func(** td2)
3016+
3017+ untyped_dict: dict[str , str ] = {}
3018+ # error: [invalid-argument-type]
3019+ func(** untyped_dict)
3020+
3021+ # error: [parameter-already-assigned]
3022+ func(v1 = 1 , ** td2)
30133023```
30143024
30153025### Assignability from unpacked kwargs to explicit keyword-only signatures
@@ -3165,6 +3175,114 @@ def stringified(**kwargs: "Unpack[StringifiedTD]") -> None:
31653175stringified(a = 1 )
31663176```
31673177
3178+ ### Non-string-keyed mappings are rejected
3179+
3180+ Only string-keyed mappings can be unpacked into named keyword parameters.
3181+
3182+ ``` py
3183+ def takes_name (* , name : str ) -> None : ...
3184+ def _ (int_key_dict : dict[int , str ]) -> None :
3185+ # snapshot: invalid-argument-type
3186+ takes_name(** int_key_dict)
3187+ ```
3188+
3189+ ``` snapshot
3190+ error[invalid-argument-type]: Argument expression after ** must be a mapping with `str` key type
3191+ --> src/mdtest_snippet.py:4:16
3192+ |
3193+ 4 | takes_name(**int_key_dict)
3194+ | ^^^^^^^^^^^^^^ Found `int`
3195+ |
3196+ ```
3197+
3198+ ### Explicit keywords still conflict with maybe-present unpacked keys
3199+
3200+ If a partial ` TypedDict ` may provide a key, passing that key explicitly still counts as a duplicate.
3201+
3202+ ``` py
3203+ from typing_extensions import TypedDict
3204+
3205+ class MaybeX (TypedDict , total = False ):
3206+ x: int
3207+
3208+ def takes_x (* , x : int ) -> None : ...
3209+ def _ (maybe_x : MaybeX) -> None :
3210+ # error: [parameter-already-assigned]
3211+ takes_x(x = 1 , ** maybe_x)
3212+ ```
3213+
3214+ ### Partial ` TypedDict ` s do not satisfy required unpacked keys
3215+
3216+ When a ` TypedDict ` key is not required, unpacking it does not prove that the corresponding required
3217+ parameter is present.
3218+
3219+ ``` py
3220+ from typing_extensions import TypedDict, Unpack
3221+
3222+ class MaybeX (TypedDict , total = False ):
3223+ x: int
3224+
3225+ class HasX (TypedDict ):
3226+ x: int
3227+
3228+ def takes_required_x (** kwargs : Unpack[HasX]) -> None : ...
3229+ def _ (maybe_x : MaybeX, has_x : HasX) -> None :
3230+ # snapshot: missing-argument
3231+ takes_required_x(** maybe_x)
3232+
3233+ takes_required_x(** has_x)
3234+ ```
3235+
3236+ ``` snapshot
3237+ error[missing-argument]: No argument provided for required parameter `x` of function `takes_required_x`
3238+ --> src/mdtest_snippet.py:12:5
3239+ |
3240+ 12 | takes_required_x(**maybe_x)
3241+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^
3242+ |
3243+ info: Parameter declared here
3244+ --> src/mdtest_snippet.py:9:22
3245+ |
3246+ 9 | def takes_required_x(**kwargs: Unpack[HasX]) -> None: ...
3247+ | ^^^^^^^^^^^^^^^^^^^^^^
3248+ |
3249+ ```
3250+
3251+ ### Partial ` TypedDict ` s can still contribute unknown keys
3252+
3253+ If a partial ` TypedDict ` only offers unrelated keys, the call can fail both because a required key
3254+ is missing and because the provided key is unknown.
3255+
3256+ ``` py
3257+ from typing_extensions import TypedDict
3258+
3259+ class MaybeExtra (TypedDict , total = False ):
3260+ extra: int
3261+
3262+ def takes_y (* , y : int ) -> None : ...
3263+ def _ (maybe_extra : MaybeExtra) -> None :
3264+ # error: [missing-argument]
3265+ # error: [unknown-argument]
3266+ takes_y(** maybe_extra)
3267+ ```
3268+
3269+ ### Legacy dunder-style positional-only parameters still coexist with unpacked keys
3270+
3271+ Legacy stub-style positional-only parameter names like ` __x ` should not conflict with unpacked
3272+ ` TypedDict ` keys of the same name.
3273+
3274+ ``` py
3275+ from typing_extensions import TypedDict, Unpack
3276+
3277+ LegacyPositionalOnlyKwargs = TypedDict(" LegacyPositionalOnlyKwargs" , {" __x" : int })
3278+
3279+ def legacy (__x : int , ** kwargs : Unpack[LegacyPositionalOnlyKwargs]) -> None :
3280+ reveal_type(kwargs) # revealed: LegacyPositionalOnlyKwargs
3281+
3282+ def _ (legacy_kwargs : LegacyPositionalOnlyKwargs) -> None :
3283+ legacy(1 , ** legacy_kwargs)
3284+ ```
3285+
31683286## Bare ` TypedDict ` annotations in ` **kwargs `
31693287
31703288A bare ` TypedDict ` annotation on ` **kwargs ` still means “arbitrary keyword names whose values have
@@ -3200,6 +3318,37 @@ def unrelated_named_parameter(x: int, **kwargs: BareKwargs) -> None:
32003318 reveal_type(kwargs) # revealed: dict[str, BareKwargs]
32013319```
32023320
3321+ ## ` dict[str, T] ` remains permissive
3322+
3323+ When the unpacked mapping is a string-keyed mapping like ` dict[str, T] ` , ty should optimistically
3324+ assume that the right keys may be present. It should still require the mapping's value type ` T ` to
3325+ be assignable to each parameter exposed by the unpacked ` TypedDict ` .
3326+
3327+ ``` py
3328+ from typing_extensions import TypedDict, Unpack
3329+
3330+ class NameKwargs (TypedDict , total = False ):
3331+ name: int
3332+
3333+ def accepts_name_kwargs (** kwargs : Unpack[NameKwargs]) -> None : ...
3334+
3335+ class AcceptsNameKwargs :
3336+ def __init__ (self , ** kwargs : Unpack[NameKwargs]) -> None :
3337+ pass
3338+
3339+ class ForwardingWrapper (AcceptsNameKwargs ):
3340+ def __init__ (self , ** kwargs : int ) -> None :
3341+ super ().__init__ (** kwargs)
3342+
3343+ def _ (good_kwargs : dict[str , int ], bad_kwargs : dict[str , str ]) -> None :
3344+ accepts_name_kwargs(** good_kwargs)
3345+ AcceptsNameKwargs(** good_kwargs)
3346+ ForwardingWrapper(** good_kwargs)
3347+
3348+ # error: [invalid-argument-type]
3349+ accepts_name_kwargs(** bad_kwargs)
3350+ ```
3351+
32033352## Recursive functional ` TypedDict ` (unstringified forward reference)
32043353
32053354Forward references in functional ` TypedDict ` calls must be stringified, since the field types are
0 commit comments