diff --git a/peps/pep-0764.rst b/peps/pep-0764.rst index e6be0fc1efc..55d93e06a13 100644 --- a/peps/pep-0764.rst +++ b/peps/pep-0764.rst @@ -1,5 +1,5 @@ PEP: 764 -Title: Inlined typed dictionaries +Title: Inline typed dictionaries Author: Victorien Plot Sponsor: Eric Traut Discussions-To: https://discuss.python.org/t/78779 @@ -20,7 +20,7 @@ typed dictionaries. In both scenarios, it requires defining a class or assigning to a value. In some situations, this can add unnecessary boilerplate, especially if the typed dictionary is only used once. -This PEP proposes the addition of a new inlined syntax, by subscripting the +This PEP proposes the addition of a new inline syntax, by subscripting the :class:`~typing.TypedDict` type:: from typing import TypedDict @@ -69,17 +69,17 @@ Taking a simple function returning some nested structured data as an example:: Rationale ========= -The new inlined syntax can be used to resolve these problems:: +The new inline syntax can be used to resolve these problems:: def get_movie() -> TypedDict[{'name': str, 'year': int, 'production': TypedDict[{'name': str, 'location': str}]}]: ... While less useful (as the functional or even the class-based syntax can be -used), inlined typed dictionaries can be assigned to a variable, as an alias:: +used), inline typed dictionaries can be assigned to a variable, as an alias:: - InlinedTD = TypedDict[{'name': str}] + InlineTD = TypedDict[{'name': str}] - def get_movie() -> InlinedTD: + def get_movie() -> InlineTD: ... @@ -96,11 +96,11 @@ comma-separated list of ``key: value`` pairs within braces constructor argument (i.e. it is not allowed to use a variable which was previously assigned a :class:`dict` instance). -Inlined typed dictionaries can be referred to as *anonymous*, meaning they +Inline typed dictionaries can be referred to as *anonymous*, meaning they don't have a specific name (see the `runtime behavior `_ section). -It is possible to define a nested inlined dictionary:: +It is possible to define a nested inline dictionary:: Movie = TypedDict[{'name': str, 'production': TypedDict[{'location': str}]}] @@ -112,16 +112,16 @@ any :term:`typing:type qualifier` can be used for individual fields:: Movie = TypedDict[{'name': NotRequired[str], 'year': ReadOnly[int]}] -Inlined typed dictionaries are implicitly *total*, meaning all keys must be +Inline typed dictionaries are implicitly *total*, meaning all keys must be present. Using the :data:`~typing.Required` type qualifier is thus redundant. -Type variables are allowed in inlined typed dictionaries, provided that they +Type variables are allowed in inline typed dictionaries, provided that they are bound to some outer scope:: class C[T]: - inlined_td: TypedDict[{'name': T}] # OK, `T` is scoped to the class `C`. + inline_td: TypedDict[{'name': T}] # OK, `T` is scoped to the class `C`. - reveal_type(C[int]().inlined_td['name']) # Revealed type is 'int' + reveal_type(C[int]().inline_td['name']) # Revealed type is 'int' def fn[T](arg: T) -> TypedDict[{'name': T}]: ... # OK: `T` is scoped to the function `fn`. @@ -129,27 +129,34 @@ are bound to some outer scope:: reveal_type(fn('a')['name']) # Revealed type is 'str' - type InlinedTD[T] = TypedDict[{'name': T}] # OK, `T` is scoped to the type alias. + type InlineTD[T] = TypedDict[{'name': T}] # OK, `T` is scoped to the type alias. T = TypeVar('T') - InlinedTD = TypedDict[{'name': T}] # OK, same as the previous type alias, but using the old-style syntax. + InlineTD = TypedDict[{'name': T}] # OK, same as the previous type alias, but using the old-style syntax. def func(): - InlinedTD = TypedDict[{'name': T}] # Not OK: `T` refers to a type variable that is not bound to the scope of `func`. + InlineTD = TypedDict[{'name': T}] # Not OK: `T` refers to a type variable that is not bound to the scope of `func`. +Inline typed dictionaries can be extended:: + + InlineTD = TypedDict[{'a': int}] + + class SubTD(InlineTD): + pass + Typing specification changes ---------------------------- -The inlined typed dictionary adds a new kind of +The inline typed dictionary adds a new kind of :term:`typing:type expression`. As such, the :external+typing:token:`~expression-grammar:type_expression` production will -be updated to include the inlined syntax: +be updated to include the inline syntax: -.. productionlist:: inlined-typed-dictionaries-grammar +.. productionlist:: inline-typed-dictionaries-grammar new-type_expression: `~expression-grammar:type_expression` : | '[' '{' (string: ':' `~expression-grammar:annotation_expression` ',')* '}' ']' : (where string is any string literal) @@ -157,11 +164,7 @@ be updated to include the inlined syntax: Runtime behavior ---------------- -Although :class:`~typing.TypedDict` is commonly referred as a class, it is -implemented as a function at runtime. To be made subscriptable, it will be -changed to be a class. - -Creating an inlined typed dictionary results in a new class, so ``T1`` and +Creating an inline typed dictionary results in a new class, so ``T1`` and ``T2`` are of the same type:: from typing import TypedDict @@ -169,8 +172,15 @@ Creating an inlined typed dictionary results in a new class, so ``T1`` and T1 = TypedDict('T1', {'a': int}) T2 = TypedDict[{'a': int}] -As inlined typed dictionaries are are meant to be *anonymous*, their -:attr:`~type.__name__` attribute will be set to an empty string. +As inline typed dictionaries are meant to be *anonymous*, their +:attr:`~type.__name__` attribute will be set to the ```` +string literal. In the future, an explicit class attribute could be added +to make them distinguishable from named classes. + +Although :class:`~typing.TypedDict` is documented as a class, the way it is +defined is an implementation detail. The implementation will have to be tweaked +so that :class:`~typing.TypedDict` can be made subscriptable. + Backwards Compatibility ======================= @@ -187,12 +197,12 @@ There are no known security consequences arising from this PEP. How to Teach This ================= -The new inlined syntax will be documented both in the :mod:`typing` module +The new inline syntax will be documented both in the :mod:`typing` module documentation and the :ref:`typing specification `. When complex dictionary structures are used, having everything defined on a single line can hurt readability. Code formatters can help by formatting the -inlined typed dictionary across multiple lines:: +inline type dictionary across multiple lines:: def edit_movie( movie: TypedDict[{ @@ -214,6 +224,8 @@ Mypy supports a similar syntax as an :option:`experimental feature {"int": int, "str": str}: return {"int": 42, "str": "test"} +Support for this PEP is added in `this pull request `__. + Pyright added support for the new syntax in version `1.1.387`_. .. _1.1.387: https://github.com/microsoft/pyright/releases/tag/1.1.387 @@ -221,7 +233,9 @@ Pyright added support for the new syntax in version `1.1.387`_. Runtime implementation ---------------------- -A draft implementation is available `here `_. +The necessary changes were first implemented in +`typing_extensions `_ +in `this pull request `__. Rejected Ideas @@ -256,7 +270,7 @@ While this would avoid having to import :class:`~typing.TypedDict` from parametrization overloads. On the other hand, :class:`~typing.TypedDict` is already a :term:`special form `. -* If future work extends what inlined typed dictionaries can do, we don't have +* If future work extends what inline typed dictionaries can do, we don't have to worry about impact of sharing the symbol with :class:`dict`. * :class:`typing.Dict` has been deprecated (although not planned for removal) @@ -288,13 +302,13 @@ Extending other typed dictionaries Several syntaxes could be used to have the ability to extend other typed dictionaries:: - InlinedBase = TypedDict[{'a': int}] + InlineBase = TypedDict[{'a': int}] - Inlined = TypedDict[InlinedBase, {'b': int}] + Inline = TypedDict[InlineBase, {'b': int}] # or, by providing a slice: - Inlined = TypedDict[{'b': int} : (InlinedBase,)] + Inline = TypedDict[{'b': int} : (InlineBase,)] -As inlined typed dictionaries are meant to only support a subset of the +As inline typed dictionaries are meant to only support a subset of the existing syntax, adding this extension mechanism isn't compelling enough to be supported, considering the added complexity. @@ -305,37 +319,11 @@ use case. Open Issues =========== -Should inlined typed dictionaries be proper classes? ----------------------------------------------------- - -The PEP currently defines inlined typed dictionaries as type objects, to be in -line with the existing syntaxes. To work around the fact that they don't have -a name, their :attr:`~type.__name__` attribute is set to an empty string. - -This is somewhat arbitrary, and an alternative name could be used as well -(e.g. ``''``). - -Alternatively, inlined typed dictionaries could be defined as instances of a -new (internal) typing class, e.g. :class:`!typing._InlinedTypedDict`. While -this solves the naming issue, it requires extra logic in the runtime -implementation to provide the introspection attributes (such as -:attr:`~typing.TypedDict.__total__`), and tools relying on runtime -introspection would have to add proper support for this new type. - -Depending on the outcome of the runtime implementation, we can more or less -easily allow extending inlined typed dictionaries:: - - InlinedTD = TypedDict[{'a': int}] - - # If `InlinedTD` is a typing._InlinedTypedDict instance, this adds complexity: - class SubTD(InlinedTD): - pass - -Inlined typed dictionaries and extra items ------------------------------------------- +Inline typed dictionaries and extra items +----------------------------------------- :pep:`728` introduces the concept of :ref:`closed ` type -dictionaries. If this PEP were to be accepted, inlined typed dictionaries +dictionaries. If this PEP were to be accepted, inline typed dictionaries will be *closed* by default. This means :pep:`728` needs to be addressed first, so that this PEP can be updated accordingly.