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 1 commit
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
104 changes: 53 additions & 51 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 <contact@vctrn.dev>
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,13 +108,16 @@ 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]}]

Inlined typed dictionaries are implicitly *total*, meaning all keys must be
present. Using the :data:`~typing.Required` type qualifier is thus redundant.

If :pep:`728` gets accepted, inlined typed dictionaries will be implicitly
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implies an ordering dependency between the two PEPs. If this PEP is accepted prior to 728, then this would represent a breaking change if and when PEP 728 is accepted — something that will likely get significant pushback. Therefore, it's probably best for this PEP to be considered for acceptance only after (or concurrently with) PEP 728's submission.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, that's why I initially put this in the open issues section. I'll move it back there, and add a note that PEP 728 needs to be addressed (either accepted or rejected) before pursuing with PEP 764.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm hoping to push PEP 728 to resolution soon.

:ref:`closed <typed-dict-closed>`.

Type variables are allowed in inlined typed dictionaries, provided that they
are bound to some outer scope::

Expand All @@ -137,11 +139,19 @@ are bound to some outer scope::

InlinedTD = TypedDict[{'name': T}] # Not OK, `T` refers to a type variable that is not bound to any scope.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with the text in this section, but the example code looks incorrect to me. The example shows a generic type alias declaration using the old-style (PEP 747) way of defining a type alias. In this case, the type variable T is bound to a valid scope — the type alias.

You can address this by changing the example code to use a form that will not be interpreted by a type checker as a type alias. For example, use a local variable (within a function body) or add a type annotation.

def func():
    InlinedTD = TypedDict[{"name": T}]
InlinedTD: object = TypedDict[{"name": T}]



It is not possible for an inlined typed dictionary to be extended::
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is extending an inlined TypedDict disallowed? I understand why we'd want to disallow extending other TypedDicts via an inlined TypedDict definition, but it's unclear why this limitation would be imposed. It seems kind of arbitrary. Perhaps there's some complication related to runtime implementation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So from a static type checking perspective, I don't think it matters really, although the usefulness is debatable. However, depending on the runtime implementation we choose to go to (either a normal TypedDict class with an arbitrary '<inlined TypedDict>' name, or an instance of a new typing._TBDClass), the runtime behavior might be difficult to implement. I'll move this in the open issues section, alongside the runtime implementation discussion.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it matters really, although the usefulness is debatable

I think we should strive to make new features consistent. Regardless of how a TypedDict is defined (the class syntax, functional syntax, or inlined syntax), the resulting type should work the same. If this limitation is left in the PEP, it means that there will be two variants of TypedDict that act differently — and therefore need to be treated by static analysis tools as slightly different beasts.


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

class SubTD(InlinedTD): # Not allowed
pass

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 +196,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 +245,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 +265,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 +288,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
---------------------------------------
Extending other typed dictionaries
----------------------------------

Should we allow the following?::

from typing import TypedDict

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


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 would most
likely 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,13 +328,6 @@ 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.

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.


Copyright
=========
Expand Down
Loading