From 04bccd670121823757c890d34ffce182c8987b28 Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Thu, 13 Feb 2025 21:36:55 +0100 Subject: [PATCH 1/2] PEP 764: Updates from discussion --- peps/pep-0728.rst | 2 + peps/pep-0764.rst | 104 +++++++++++++++++++++++----------------------- 2 files changed, 55 insertions(+), 51 deletions(-) diff --git a/peps/pep-0728.rst b/peps/pep-0728.rst index 3557d0920a2..0f645690404 100644 --- a/peps/pep-0728.rst +++ b/peps/pep-0728.rst @@ -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 ------------------------------ diff --git a/peps/pep-0764.rst b/peps/pep-0764.rst index 9e98479a8e1..cc9e9745745 100644 --- a/peps/pep-0764.rst +++ b/peps/pep-0764.rst @@ -2,11 +2,13 @@ PEP: 764 Title: Inlined typed dictionaries Author: Victorien Plot Sponsor: Eric Traut +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 `__ 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 ` (the dictionary keys are strings representing the field names, and values are valid :ref:`annotation expressions `). 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 `_ +don't have a specific name (see the `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 `. + 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. + +It is not possible for an inlined typed dictionary to be extended:: + + 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 `. -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 ========= From 764f867711447e91da304eb1767d325f803a613c Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Fri, 14 Feb 2025 17:38:15 +0100 Subject: [PATCH 2/2] Feedback --- peps/pep-0764.rst | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/peps/pep-0764.rst b/peps/pep-0764.rst index cc9e9745745..e6be0fc1efc 100644 --- a/peps/pep-0764.rst +++ b/peps/pep-0764.rst @@ -115,9 +115,6 @@ any :term:`typing:type qualifier` can be used for individual fields:: 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 `. - Type variables are allowed in inlined typed dictionaries, provided that they are bound to some outer scope:: @@ -137,15 +134,12 @@ 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. -It is not possible for an inlined typed dictionary to be extended:: - InlinedTD = TypedDict[{'a': int}] + def func(): + InlinedTD = TypedDict[{'name': T}] # Not OK: `T` refers to a type variable that is not bound to the scope of `func`. - class SubTD(InlinedTD): # Not allowed - pass Typing specification changes ---------------------------- @@ -304,8 +298,8 @@ 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. -If intersections were to be added into the type system, it would most -likely cover this use case. +If intersections were to be added into the type system, it could cover this +use case. Open Issues @@ -328,6 +322,23 @@ 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 :ref:`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 =========