PEP 764: Inlined typed dictionaries

The PEP rejects inheritance (InlinedBase = TypedDict[{'a': int}]; Inlined = TypedDict[InlinedBase, {'b': int}]) and says that intersections could handle this use case. Presumably you’d write something like InlinedBase & TypedDict[{'b': int}].

However, the PEP also says that if PEP 728 is accepted, inline TypedDicts will be closed by default. However, closed TypedDicts cannot have extra keys, so the suggested intersection type would be equivalent to Never. Therefore, I don’t think intersections provide a solution here.

2 Likes

Shouldn’t the result of the intersection be read as a new derived type merging the keys from all involved types? The example you provided would effectively result in a new, independent closed TypedDict[{'a': int, 'b': int}], as both input types are closed.
At least in other typing systems that have intersections (e.g. TS), it works this way.[1]


  1. Things would get a bit more complicated if we ever consider intersections between TypedDict classes and inlined TypedDicts, but that’s a topic for another discussion. ↩︎

Not if it’s a closed TypedDict. TypedDict[{"b": int}] means a TypedDict that has the key b and no others. TypedDict[{"b": int}] & TypedDict[{"a": int}] therefore means a dict that has the key b and no others, that is also a dict that has the key a and no others. Such a dict cannot exist.

9 Likes

What was the reason for inlined typed dictionaries being closed by default? I didn’t see the rationale in the text of PEP 764 or in this discussion.

According to PEP 728,

  1. A TypedDict which doesn’t set closed=True (so every TypedDict today) has a behaviour implied by closed=False
  2. closed=True is equivalent to extra_items=Never
  3. extra_items=str is equivalent to <TypedDict> & Mapping[str, str]

That implies it’s more consistent (with standard TypedDicts) and more feature-composable to have inlined typed dictionaries as closed=False, then when intersections are introduced, you can close them by additionally intersecting with & Mapping[str, Never] or have extra items by intersecting with & Mapping[str, ExtraItemType].

Hi, author of the current intersection draft here, Intersecting with Mapping[str, Never] would mean that for keys that are string, there can’t be a runtime value, this isn’t your intent here, and these won’t work to close typed dicts.

I’m not sure where some of the misconceptions about intersections are coming from, but I’ve seen multiple in this thread, and they won’t assist with the closed/extra values bit, and issues regarding them should not be punted to intersections, which will work Solely off of logical application of subtyping. Addressing these questions may require new type forms (ie. Closed[TypedDict[{key-name: ValueType}]] to create a closed version inline) or new syntax (ie. TypedDict[{key-name: ValueType}, closed=True])

3 Likes

Would something like this be too ugly to support additional arguments?

TypedDict[{"a": str}, {"closed":True}]
# or
TypedDict[{"a": str}, TypedDictArgs(closed=True)]
1 Like

I think the Specification section could be reworded a little to say that specifically, only a dict display is allowed as the type argument to TypedDict:

Specification

The TypedDict special form is made subscriptable, and accepts a single type argument which must be a dict display, …

The part about disallowing a dict() and variables would then become redundant.

This was suggested by @erictraut early in the GH PR discussion. I think one argument in favor of this is that closed=True is the behavior that most users want (see the numerous issues on mypy/pyright’s issue trackers about items() being typed as dict[str, object]). Also the way I see it, inline typed dictionaries are meant for one-off definitions (e.g. for a function parameter), and its scope is thus well known to the developer [1].

Considering the potential incompatibility with intersections, I’m also happy to switch back to closed=False to be the default, but @erictraut might have more arguments in favor of having closed-ness to be the default.

The dict_display production also includes dict_comprehension and ** unpacking, which isn’t allowed in the inline syntax.


Regarding having a way to specify totality/closed-ness/etc, I’ll refer to this comment: the inline syntax isn’t meant to support all features from the existing class/functional-based syntaxes. I’m also already not too satisfied with the current syntax (TypedDict[{...}]) and envy TypeScript’s capabilities.

On that note – before we go with this syntax – do you think there will be a future where new syntax could be used in type annotations (especially as in 3.14 they are now lazily evaluated)? A syntax with better ergonomics, that wouldn’t necessarily be valid Python syntax but would allow expressing typing concepts in a concise and (more) readable way?


  1. If I use an inline TD for a parameter of a function I define, most of the time I expect no extra items to be present. ↩︎

2 Likes

Thank you for the explanation.

Apart from incompatibility with intersections, I believe that “closedness” is also dependent on PEP 728 being accepted, because that PEP introduces the concept. This leaves inlined typed dictionaries with unpredictable or backwards-incompatible behaviour if PEP 728 was rejected then revisited and accepted later, or it leaves this PEP as stuck until PEP 728 is accepted to allow “closedness” to be specified.

I would also think that “inline syntax isn’t meant to support all features” as more naturally indicating that the inline syntax is equivalent to not specifying any class keywords which modify the meaning of a standard TypedDict, such as closed=True (PEP 728 indicates that closed=False / extra_items=ReadOnly[object] is the default behaviour of existing TypedDicts).

A small correction that from PEP728 default TypedDict behaviour is closed=False and extra_items unset. extra_items=ReadOnly[object] is used only for inheritance and assignability checks.

1 Like

I know the PEP has already rejected using builtins.dict but given that dictionaries are extremely common, and inline typed dictionaries are designed to address the scenario where a dictionary has a fixed shape but isn’t complex or reused enough to warrant a named class, I think allowing plain dict for this inlined syntax is significantly more important. If defining a named class is already seen as too much overhead for a particular structure, requiring an import of typing.TypedDict might still be perceived as a barrier. I also agree that the inline syntax can be extremely limited in functionality to start off.

1 Like

The Python 3.14 changes don’t affect the syntax, only how that syntax is parsed. Type annotations are still just expressions. We could potentially change that, of course, but there isn’t any particularly obvious way to do it.

There are also still some places where type expressions can appear in places that are not lazily evaluated, e.g. type arguments to base classes and arguments to cast() and NewType().

Still, it could be good to think about what syntax we’d want if we had a magic wand and could add new syntax to the language. It’s going to be a harder sell than adding this feature without new syntax, but I don’t think it’s out of the question that a syntax change would be accepted.

When I initially suggested that we consider making inlined TypedDicts “closed” by default, I hadn’t considered the implications for intersections. I don’t object to switching back to closed=False for inlined TypedDicts if there is consensus around that point. There are advantages and disadvantages to doing this, so I think the PEP author(s) should weigh the tradeoffs and pick the one that best serves the majority of use cases. For less-common use cases, traditional (non-inlined) TypedDict definitions can always be used.

1 Like

I’m going to suggest that you not choose closed=True/False default based on intersections. it’s important to acknowledge that intersections won’t solve all possible things here, but theres always the non-inline form + type aliases to ensure this isn’t closing off options for users.

If any ergonomic shortcomings are discovered and are frequent enough to warrant improving the inline interaction with intersections when they reach users, there’s always room for that to be improved later, either with new constructs (for instance a Closed/Open typeform that creates a new structural type from a typed-dict that is the same, but is closed/open, Pick to copy specific fields into a new structural type), or by revisiting the ability to index with keyword arguments as a syntax alteration when there is evidence it will be worth allowing.

4 Likes

Then I’m willing to keep having closed=True the default, considering what I mentioned above. This deviates from the existing forms so can be confusing, but it is an opportunity to chose a better default without breaking changes.

6 Likes

Maybe we could consider defining that the intersection is distributive over the closed property? So if you picture that some typed dict D is closed when written as Closed[D], then you’d have that Closed[T1] & Closed[T2] equivs Closed[T1 & T2]. [1] In form, it’s a bit like type[A] | type[B] ~ type[A | B] this way.

Theoretically it feels a bit awkward (but maybe that’s just me). But in practice, I can imagine it being more useful like this :person_shrugging:


  1. Just to be clear: I’m not actually proposing a Closed; it’s just an ad-hoc notation to help illustrate what I mean. ↩︎

That would break consistent definitions for intersections. If we actually need a way to compose typed dicts that isn’t either intersections or the existing subclassing method of extending typeddicts, that should be proposed seperately by those who need it, without making another type system feature less consistent.

3 Likes

Great PEP! I can’t wait to see it accepted and get rid of the bazillion of dataclasses and TypedDicts in our code base just to type structured data.

On the topic of open-/closedness and intersections, assuming that (something like) PEP 728 gets accepted:

  • If I had to wager a guess, I’d say closed dictionary types will likely see more usage than open ones, since they provide so much more type safety in practice (.keys(), .values(), .items(), et cetera). In TypeScript I regularly want to scream because of the lack of type safety of Object.keys(), Object.entries() etc. In other words: Inline TypedDicts being closed by default makes sense to me.

  • That being said, if the class-based syntax for defining TypedDict is open by default, there is something to be said about consistency and making the inline syntax open by default, too.

  • Intersections of types are not necessarily the same thing as building a new type from existing ones. In the present case, while the intersection of two closed TypedDicts A & B won’t make much sense if A and B don’t share some entries, we could think of a syntax for merging just the structure definitions, e.g. something like TypedDict[{ **A, **B }] or TypedDict[Structure[A] | Structure[B]], where Structure[] returns just the TypedDict’s underlying dictionary and | is the usual dictionary concatenation operator. (Yes, not exactly the greatest syntax given that | is also used for type union.)

  • If (inline) TypedDicts end up being closed by default, one could also think of indicating openness through an auxiliary dictionary item, e.g. TypedDict[{ 'a': str, 'b': int, ...: Any}]. Though of course using an ellipsis ... as key here would rule out using the ellipsis as key in the actual data later. Maybe TypedDict[{ 'a': str, 'b': int, TKeyType: Any}] would be the better choice? Here, TKeyType could be any type extending Hashable and would describe the type of keys of extra items, e.g. str?

Maybe we could have the __(class_)getitem__ accept either one or two arguments, like TypedDict[{}, True] or TypedDict[{}, False] to close / open the TypedDict? Alternatively we could set closing by some TypedDict[{}].closed() and TypedDict[{}].open() (or maybe other names, for sake of not applinging to any Protocols / ABCs with Duck typing).

Apart from that I guess that’s the only thing I’d change about the proposed changes here.

1 Like

As I mentioned earlier:

That could be a nice alternative. It wasn’t possible to use the ellipsis in PEP 728 as it is illegal as a annotation target, but would work for inline TDs. I’ll consider this option once PEP 728 is decided on, but I’ll refer again to my comment regarding the capabilities of the inline syntax.

Typed dictionaries can only have strings as keys, so this isn’t an issue.

Same as above, I’ll refer to my comment.

1 Like