Proposal: Support `Unpack[dict[...]]` for typing `**keyword` arguments

(Originally reported to mypy issue tracker: Support `Unpack` with `dict` for keyword arguments: `**kwargs: Unpack[dict[Literal["test"], ...]]` · Issue #17241 · python/mypy · GitHub)

Currently, PEP 692 specifies that **kwargs: Unpack[T] only works where T is a TypedDict, in no uncertain terms PEP 692 – Using TypedDict for more precise **kwargs typing | peps.python.org “using Unpack with types other than TypedDict should not be allowed and type checkers should generate errors in such cases”

I think there are use cases for expanding the types allowed by Unpack. I realize that this change would require writing a PEP and I’m not interested myself in championing such a PEP. But I wanted to float the idea regardless.

Dict keys defined as Literal[]

One example is when dealing with dicts with uniform value types. When I declare allowed dict keys as a Literal[] type alias anyway: requiring me to also duplicate the whole list as a TypedDict is extra work and maintenance. For example:

ExampleKeys: TypeAlias = Literal["example_key", "foo"]

def example(**kwargs: Unpack[dict[ExampleKeys, int]]): ...
    #                        ^^^^

Right now type checkers require me to duplicate this as:

class ExampleKeysDict(TypedDict):
    example_key: int
    foo: int

def example(**kwargs: Unpack[ExampleKeysDict]): ...

And that causes new problems, at least with mypy: ExampleKeysDict.keys() is no longer constrained as Literal[] type and ExampleKeysDict.values() is no longer constrained to int. (Because in the general case, subclasses of ExampleKeysDict or other compatible types may exist, that can contain extra keys with other value types that aren’t known at the point of use. There is an open issue in mypy to solve most cases of this with a @final decorator; final TypedDict · Issue #7981 · python/mypy · GitHub, but that does not solve the duplication aspect).

Dict keys as LiteralString

Another use case might be to allow arbitrary dict keys, but constraining the keys to LiteralString:

def example(**kwargs: Unpack[dict[LiteralString, int]]): ...

Unpack with other types

Also if this is implemented already, I see no reason to reject Mapping/MutableMapping as well, although implementations would always use dict. Maybe one already has defined a type alias for such Mapping that can be re-used.

ExampleMapping: TypeAlias = Mapping[ExampleKeys, int]

def example(**kwargs: Unpack[ExampleMapping]): ...

Some of these issues might be solved with PEP 728 – TypedDict with Typed Extra Items | peps.python.org

Nope, doesn’t really affect this AFAICT.

__extra_items__ only allows constraining dict value types, but this proposal is about constraining dict key types.

I get your first use-case if we’re talking about a large set of keys, for anything else it seems better to just write the signature instead, even if that means you’re forced to provide a default value for those arguments.

Your second use-case sounds nonsensical to me, since it is already implied to a degree, unless you want the behavior to be that people can’t unpack a mapping into that callable if it contains non-literal keys. Which just seems like hostile API design and making type checkers work hard for very little benefit.

The third use-case is in a similar boat, since you are unpacking, what container type you are unpacking from is essentially irrelevant, all that matters are the key value type pairs. Allowing it is a little more ergonomic, since it avoids potentially duplicating a type, but it also makes me a little nervous. By the same logic you could allow arbitrary types satisfying the Iterable protocol for Unpack/* in varargs and generics that accept a TypeVarTuple. This makes the job of type checkers more difficult and some annotations more difficult to parse for humans[1].


  1. so only allowing dict seems more in line with what we can do with tuple and TypeVarTuple ↩︎

This can be rectified by closing the key space with PEP 728.

1 Like

Ah, thanks for ponting that out. Yes, closed=True solves that, I missed that part of the spec when skimming it.