I agree with Eric that `**kwargs: Foo` means that `kwargs` is a dictionary of Foo values, as per PEP 484. Changing that would be backward-incompatible, and special-casing it for TypedDict alone would be clumsy.
However, it *is* useful to be able to specify the individual types of the keyword-only parameters we want to accept.
*Proposal*: We allow typing `**kwargs: **KwargsTypedDict`.
Required keyword-only parameters will be required fields of the TypedDict. Keyword-only parameters with default values will be non-required fields of the TypedDict.
I wanted a real-world example to motivate this and thought of `json.loads` and co:
```python
# Simplified and modified slightly from `json.loads` and `json.load`, which
# share the same keyword-only parameters.
# Before.
def loads(
s: Union[str, bytes],
*,
# No default value for this one.
json_decoder: Type[JSONDecoder],
# Has a default value.
parse_int: Optional[Callable[[str], Any]] = ...,
# And a bunch of other keyword-only parameters.
) -> JSON:
...
def load(
fp: SupportsRead[Union[str, bytes]],
*,
json_decoder: Type[JSONDecoder],
parse_int: Optional[Callable[[str], Any]] = ...,
) -> JSON:
...
class JSONDecoder:
def __init__(
self,
*,
parse_int: Optional[Callable[[str], Any]] = ...,
) -> None: ...
```
Note that `loads` and `load` internally just construct a `JSONDecoder` by passing on their kwargs to the given `json_decoder`. Clearly, these functions share the same keyword-only parameters.
```python
# These keyword-only parameters had default values. So, they are non-required
# fields in the TypedDict.
class JSONDecoderKwargs(TypedDict, total=False):
parse_int: Callable[[str], Any]
# And a bunch of others.
# `json_decoder` was a required keyword-only parameter (in my example above).
# So, it goes in a total TypedDict.
# We also inherit the other fields (preserving their non-requiredness).
class LoadKwargs(JSONDecoderKwargs, total=True):
json_decoder: Type[JSONDecoder]
# After.
def loads(
s: Union[str, bytes],
**kwargs: **LoadKwargs,
) -> JSON:
...
def load(
fp: SupportsRead[Union[str, bytes]],
**kwargs: **LoadKwargs,
) -> JSON:
...
class JSONDecoder:
def __init__(
self,
**kwargs: **JSONDecoderKwargs,
) -> None: ...
# valid
loads(s, json_decoder=MyJsonDecoder)
loads(s, json_decoder=MyJsonDecoder, parse_int=my_parse_int)
# invalid: missing keyword-only parameter `class_info`.
loads(s)
# invalid: type mismatch
loads(s, json_decoder=1)
# invalid: unexpected argument `foo`.
loads(s, json_decoder=MyJsonDecoder, foo=2)
```
Other such cases off the top of my head include
+ `subprocess.run`, `Popen.__init__`, and friends.
+ `sort` and `sorted`, which accept the same keyword-only parameters
Things to consider:
1. This is backward-compatible because it preserves the `**kwargs: int` behavior of PEP 484.
2. `**kwargs: **KwargsTypedDict` requires changes to the parser. If that's not worth it, we could settle for something like `**kwargs: UnpackTypedDict[KwargsTypedDict]`. This is analogous to PEP 646 making `Unpack[Ts]` a synonym for `*Ts` until syntax support lands.
3. We may also want to allow arbitrary keyword parameters beyond the ones specifically named.
That is, how would we type the following using the above TypedDict proposal?
```
def foo(*, required: int, non_required: str=..., **kwargs: int) -> None: ...
# valid.
foo(required=1)
# valid.
foo(required=1, extra=7)
# invalid: expected int, got str.
foo(required=1, extra="wrong type")
```
One option is to simply require users to type out the named keyword-only parameters by hand (as done above for `foo`). This wouldn't allow multiple such functions to share the same keyword-only parameters, but would not require any other changes.
A more long-term option is to allow open-ended TypedDicts - ones that allows arbitrary fields other than the named fields. I believe there was some discussion about this earlier, but there was no resulting PEP there:
https://mail.python.org/archives/list/typing-sig@python.org/thread/66RITIHDQHVTUMJHH2ORSNWZ6DOPM367/#2S4YVCLI2FNMSP7QJTSDRV5VUVF27LEM. This might be impractical to wait for.
Yet another option is to always allow arbitrary kwargs beyond the fields in the TypedDict. I'm against this because it won't let us specify that we want a finite set of keyword-only parameters, like in the `json.loads` example above.
In any case, this is a backward-compatible feature that we can defer for this discussion.