PEP 764: Inlined typed dictionaries

If you agree, I’ll add you as a sponsor in the next update.

I would be happy to! :slight_smile:

From my past experience, it is a good idea to limit the scope of this feature to a bare minimum. We can always add something when needed (like & syntax / inheritance / etc)

I still feel like the call expression syntax makes more sense:

def get_movie() -> TypedDict({'title': str, 'year': int}): ...

because so far in Python typing, square brackets only ever contain types – even in special forms: Required[int] – but the dictionary is not a type.

The PEP says this in rejected ideas:

However, call expressions are currently unsupported in such a context for various reasons (expensive to process, evaluating them is not standardized).

But I don’t really understand what this is trying to say. Is this about PEP 649?

Not in Literal.

This should work fine with PEP 649.

>>> def f(x) -> TypedDict({"x": int}): pass
... 
>>> import annotationlib, inspect
>>> inspect.signature(f, annotation_format=annotationlib.Format.STRING)
<Signature (x) -> "TypedDict({'x': int})">

The concern is about implementation complexity in type checkers.

2 Likes

That’s a great idea!

But now that we’ve done so much with TypedDict, can we try to use a lot of Typescript syntax?

Such as

  • Readable & Writeable
  • keyof TypeA
  • {[key in keyof TypeA]: readonly TypeA[key]}
1 Like

See the following threads:

3 Likes

I like this proposal. Tbh I’m not a huge fan of TypedDict in general, but given that we seem to be stuck with it, this would make declaring simple TypedDicts much more ergonomic.

My 2c on the open issues section:

2 Likes

One thing I see as missing (either from the spec, or as a rejected idea) is how this is expected to play with **kwargs annotations.

Ideally, the following would work:

def foo(**kwargs: Unpack[TypedDict[{'name': str, 'year': NotRequired[int]}]]): ...

as an expressive way to declare some keyword args are (truly) not required (as opposed to the alternatives today)

def foo(*, name: str, year : int | None = None): ...
# or, maybe if `None` has meaning in the API
def foo(*, name: str, year : int | OMITTED_T = OMITTED): ...

# or the increasingly-verbose
@overload
def foo(*, name: str): ...
@overload
def foo(*, name: str, year : int): ...
def foo(*, name: str, **kwargs): ...

typing.Unpack can be used to do so already, and I don’t think there’s anything blocking from using it with the inlined form:

def foo(**kwargs: Unpack[TypedDict[{'name': str, 'year': NotRequired[int]}]]): ...

If this is too verbose, we could also allow:

def foo(**kwargs: *TypedDict[{'name': str, 'year': NotRequired[int]}]): ...

Although there might be runtime implications with this one (it is currently invalid code).

Yeah, sorry, typo (I always forget the Unpack bit, I’ll edit).

This is exactly what I was suggesting be pointed out in the proposal. That it is expected for it to “just work” in this manner. (Otherwise without an example, all I see is return-type examples, so it could be argued its ambiguous)

I pushed PEP 764: Updates from discussion by Viicos · Pull Request #4270 · python/peps · GitHub, addressing most of the feedback (I still need to experiment with the runtime implementation).

I think this is already covered by the annotation_expression definition:

| <Unpack> '[' name ']'
  (where name refers to an in-scope TypedDict;
  valid only in **kwargs annotations)

Alternatively, it could be updated to state to an in-scope or inlined TypedDict. Let me know on the PR, I can add this edit (I also added an example of an inlined typed dictionary used as a parameter type).

1 Like