Skip to content
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

PEP 764: Updates from discussion #4270

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
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
2 changes: 2 additions & 0 deletions peps/pep-0728.rst
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@ must be assignable to the value of ``extra_items`` defined on ``MovieBase``.

Movie = TypedDict("Movie", {"name": str}, extra_items=int | None)

.. _typed-dict-closed:

The ``closed`` Class Parameter
------------------------------

Expand Down
109 changes: 61 additions & 48 deletions peps/pep-0764.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ PEP: 764
Title: Inlined typed dictionaries
Author: Victorien Plot <[email protected]>
Sponsor: Eric Traut <erictr at microsoft.com>
Discussions-To: https://discuss.python.org/t/78779
Status: Draft
Type: Standards Track
Topic: Typing
Created: 25-Oct-2024
Python-Version: 3.14
Post-History: `29-Jan-2025 <https://discuss.python.org/t/78779>`__


Abstract
Expand Down Expand Up @@ -72,9 +74,6 @@ The new inlined syntax can be used to resolve these problems::
def get_movie() -> TypedDict[{'name': str, 'year': int, 'production': TypedDict[{'name': str, 'location': str}]}]:
...

It is recommended to *only* make use of inlined typed dictionaries when the
structured data isn't too large, as this can quickly become hard to read.

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::

Expand All @@ -87,8 +86,8 @@ used), inlined typed dictionaries can be assigned to a variable, as an alias::
Specification
=============

The :class:`~typing.TypedDict` class is made subscriptable, and accepts a
single type argument which must be a :class:`dict`, following the same
The :class:`~typing.TypedDict` special form is made subscriptable, and accepts
a single type argument which must be a :class:`dict`, following the same
semantics as the :ref:`functional syntax <typing:typeddict-functional-syntax>`
(the dictionary keys are strings representing the field names, and values are
valid :ref:`annotation expressions <typing:annotation-expression>`). Only the
Expand All @@ -98,7 +97,7 @@ 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
don't have a name (see the `runtime behavior <Runtime behavior>`_
don't have a specific name (see the `runtime behavior <Runtime behavior>`_
section).

It is possible to define a nested inlined dictionary::
Expand All @@ -109,7 +108,7 @@ It is possible to define a nested inlined dictionary::
Movie = TypedDict[{'name': str, 'production': {'location': str}}]

Although it is not possible to specify any class arguments such as ``total``,
any :external+typing:term:`type qualifier` can be used for individual fields::
any :term:`typing:type qualifier` can be used for individual fields::

Movie = TypedDict[{'name': NotRequired[str], 'year': ReadOnly[int]}]

Expand All @@ -135,13 +134,18 @@ are bound to some outer scope::

T = TypeVar('T')

InlinedTD = TypedDict[{'name': T}] # Not OK, `T` refers to a type variable that is not bound to any scope.
InlinedTD = 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`.


Typing specification changes
----------------------------

The inlined typed dictionary adds a new kind of
:external+typing:term:`type expression`. As such, the
:term:`typing:type expression`. As such, the
:external+typing:token:`~expression-grammar:type_expression` production will
be updated to include the inlined syntax:

Expand Down Expand Up @@ -186,8 +190,20 @@ How to Teach This
The new inlined syntax will be documented both in the :mod:`typing` module
documentation and the :ref:`typing specification <typing:typed-dictionaries>`.

As mentioned in the `Rationale`_, it should be mentioned that inlined typed
dictionaries should be used for small structured data to not hurt readability.
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::

def edit_movie(
movie: TypedDict[{
'name': str,
'year': int,
'production': TypedDict[{
'location': str,
}],
}],
) -> None:
...


Reference Implementation
Expand Down Expand Up @@ -223,11 +239,11 @@ various reasons (expensive to process, evaluating them is not standardized).

This would also require a name which is sometimes not relevant.

Using ``dict`` with a single type argument
------------------------------------------
Using ``dict`` or ``typing.Dict`` with a single type argument
-------------------------------------------------------------

We could reuse :class:`dict` with a single type argument to express the same
concept::
We could reuse :class:`dict` or :class:`typing.Dict` with a single type
argument to express the same concept::

def get_movie() -> dict[{'title': str}]: ...

Expand All @@ -243,6 +259,10 @@ While this would avoid having to import :class:`~typing.TypedDict` from
* If future work extends what inlined 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)
by :pep:`585`. Having it used for a new typing feature would be confusing
for users (and would require changes in code linters).

Using a simple dictionary
-------------------------

Expand All @@ -262,45 +282,28 @@ cases incompatible, especially for runtime introspection::
# Raises a type error at runtime:
def fn() -> {'a': int} | int: ...

Open Issues
===========

Subclassing an inlined typed dictionary
---------------------------------------

Should we allow the following?::

from typing import TypedDict

InlinedTD = TypedDict[{'a': int}]
Extending other typed dictionaries
----------------------------------


class SubTD(InlinedTD):
pass

What about defining an inlined typed dictionay extending another typed
dictionary?::
Several syntaxes could be used to have the ability to extend other typed
dictionaries::

InlinedBase = TypedDict[{'a': int}]

Inlined = TypedDict[InlinedBase, {'b': int}]
# or, by providing a slice:
Inlined = TypedDict[{'b': int} : (InlinedBase,)]

Using ``typing.Dict`` with a single argument
--------------------------------------------
As inlined 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.

While using :class:`dict` isn't ideal, we could make use of
:class:`typing.Dict` with a single argument::
If intersections were to be added into the type system, it could cover this
use case.

def get_movie() -> Dict[{'title': str}]: ...

It is less verbose, doesn't have the baggage of :class:`dict`, and is
already defined as some kind of special form.

However, it is currently marked as deprecated (although not scheduled for
removal), so it might be confusing to undeprecate it.

This would also set a precedent on typing constructs being parametrizable
with a different number of type arguments.
Open Issues
===========

Should inlined typed dictionaries be proper classes?
----------------------------------------------------
Expand All @@ -319,12 +322,22 @@ 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
------------------------------------------

:pep:`728` introduces the concept of *closed* type dictionaries. Inlined
typed dictionaries should probably be implicitly *closed*, but it may be
better to wait for :pep:`728` to be accepted first.
:pep:`728` introduces the concept of :ref:`closed <typed-dict-closed>` type
dictionaries. If this PEP were to be accepted, inlined typed dictionaries
will be *closed* by default. This means :pep:`728` needs to be addressed
first, so that this PEP can be updated accordingly.


Copyright
Expand Down