-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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:: | ||
|
||
|
@@ -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 | ||
|
@@ -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:: | ||
|
@@ -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 | ||
:ref:`closed <typed-dict-closed>`. | ||
|
||
Type variables are allowed in inlined typed dictionaries, provided that they | ||
are bound to some outer scope:: | ||
|
||
|
@@ -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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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:: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think we should strive to make new features consistent. Regardless of how a TypedDict is defined (the |
||
|
||
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: | ||
|
||
|
@@ -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 | ||
|
@@ -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}]: ... | ||
|
||
|
@@ -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 | ||
------------------------- | ||
|
||
|
@@ -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? | ||
---------------------------------------------------- | ||
|
@@ -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 | ||
========= | ||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.