-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
PEP 764: Updates from discussion #4270
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
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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,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]}] | ||
|
||
|
@@ -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: | ||
|
||
|
@@ -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 | ||
|
@@ -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}]: ... | ||
|
||
|
@@ -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 | ||
------------------------- | ||
|
||
|
@@ -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? | ||
---------------------------------------------------- | ||
|
@@ -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 | ||
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Black and ruff would reformat this as
TypedDict[{'b': int}: (InlinedBase,)]
. And even though it's now formatted exactly like in my proposal, it might be worth to consider a black-compatible formatting instead 🤷🏻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.
We can just change this in Black, current behavior of formatters shouldn't unduly influence language evolution.
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.
even better!
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.
Note that this is part of the rejected ideas, as per https://discuss.python.org/t/78779/18.