Skip to content

PEP 764: Second round of updates #4386

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 50 additions & 62 deletions peps/pep-0764.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
PEP: 764
Title: Inlined typed dictionaries
Title: Inline typed dictionaries
Author: Victorien Plot <[email protected]>
Sponsor: Eric Traut <erictr at microsoft.com>
Discussions-To: https://discuss.python.org/t/78779
Expand All @@ -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
Expand Down Expand Up @@ -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:
...


Expand All @@ -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 <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}]}]

Expand All @@ -112,65 +112,75 @@ 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`.

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`
: | <TypedDict> '[' '{' (string: ':' `~expression-grammar:annotation_expression` ',')* '}' ']'
: (where string is any string literal)

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

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 ``<inline TypedDict>``
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
=======================
Expand All @@ -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 <typing:typed-dictionaries>`.

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[{
Expand All @@ -214,14 +224,18 @@ Mypy supports a similar syntax as an :option:`experimental feature <mypy:mypy.--
def test_values() -> {"int": int, "str": str}:
return {"int": 42, "str": "test"}

Support for this PEP is added in `this pull request <https://github.com/python/mypy/pull/18889>`__.

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

Runtime implementation
----------------------

A draft implementation is available `here <https://github.com/Viicos/cpython/commit/49e5a83f>`_.
The necessary changes were first implemented in
`typing_extensions <https://typing-extensions.readthedocs.io/en/latest/>`_
in `this pull request <https://github.com/python/typing_extensions/pull/580>`__.


Rejected Ideas
Expand Down Expand Up @@ -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 <typing: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)
Expand Down Expand Up @@ -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.

Expand All @@ -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. ``'<TypedDict>'``).

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 <typed-dict-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.

Expand Down