Skip to content

Commit c058d0e

Browse files
committed
Applied remaining feedback
Some GH discussions are left opened for now, waiting for an answer.
1 parent ac5786b commit c058d0e

File tree

1 file changed

+90
-16
lines changed

1 file changed

+90
-16
lines changed

peps/pep-9995.rst

Lines changed: 90 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Discussions-To:
55
Status: Draft
66
Type: Standards Track
77
Topic: Typing
8-
Created: 23-Oct-2024
8+
Created: 25-Oct-2024
99
Post-History:
1010
Python-Version: 3.14
1111
Resolution:
@@ -73,16 +73,17 @@ Rationale
7373

7474
The new inlined syntax can be used to resolve these problems::
7575

76-
def get_movie() -> TypedDict[{'name': str, year: int, 'production': {'name': str, 'location': str}}]:
76+
def get_movie() -> TypedDict[{'name': str, 'year': int, 'production': TypedDict[{'name': str, 'location': str}]}]:
7777
...
7878

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

8282
While less useful (as the functional or even the class-based syntax can be
83-
used), inlined typed dictionaries can be defined as a type alias::
83+
used), inlined typed dictionaries can be assigned to a variable, as an alias::
84+
85+
InlinedTD = TypedDict[{'name': str}]
8486

85-
type InlinedDict = TypedDict[{'name': str}]
8687

8788
Specification
8889
=============
@@ -97,7 +98,43 @@ Inlined typed dictionaries can be referred as being *anonymous*, meaning they
9798
don't have a name. For this reason, their :attr:`~type.__name__` attribute
9899
will be set to an empty string.
99100

100-
It is not possible to specify any class arguments such as ``total``.
101+
It is possible to define a nested inlined dictionary::
102+
103+
Movie = TypedDict[{'name': str, 'production': TypedDict[{'location': str}]}]
104+
105+
# Note that the following is invalid as per the updated `type_expression` production:
106+
Movie = TypedDict[{'name': str, 'production': {'location': str}}]
107+
108+
Although it is not possible to specify any class arguments such as ``total``,
109+
Any :external+typing:term:`type qualifier` can be used for individual fields::
110+
111+
Movie = TypedDict[{'name': NotRequired[str], 'year': ReadOnly[int]}]
112+
113+
Inlined typed dictionaries are implicitly *total*, meaning all keys must be
114+
present. Using the :data:`~typing.Required` type qualifier is thus redundant.
115+
116+
Type variables are allowed in inlined typed dictionaries, provided that they
117+
are bound to some outer scope::
118+
119+
class C[T]:
120+
inlined_td: TypedDict[{'name': T}] # OK, `T` is scoped to the class `C`.
121+
122+
reveal_type(C[int]().inlined_td['name']) # Revealed type is 'int'
123+
124+
125+
def fn[T](arg: T) -> TypedDict[{'name': T}]: ... # OK: `T` is scoped to the function `fn`.
126+
127+
reveal_type(fn('a')['name']) # Revealed type is 'str'
128+
129+
130+
type InlinedTD[T] = TypedDict[{'name': T}] # OK
131+
132+
133+
T = TypeVar('T')
134+
135+
InlinedTD = TypedDict[{'name': T}] # Not OK, `T` refers to a type variable that is not bound to any scope.
136+
137+
**TODO** closed
101138

102139
Runtime behavior
103140
----------------
@@ -107,7 +144,7 @@ implemented as a function at runtime. To be made subscriptable, it will be
107144
changed to be a class.
108145

109146
Creating an inlined typed dictionary results in a new class, so both syntaxes
110-
return the same type::
147+
return the same type (apart from the different :attr:`~type.__name__`)::
111148

112149
from typing import TypedDict
113150

@@ -117,12 +154,15 @@ return the same type::
117154
Typing specification changes
118155
----------------------------
119156

120-
The inlined typed dictionary syntax adds a new valid location for
121-
:term:`type expressions <typing:type expression>`. As such, the specification
122-
on :ref:`valid locations <typing:valid-types>` will need to be updated, most
123-
likely by adding a new item to the list:
157+
The inlined typed dictionary adds a new kind of
158+
:term:`type expressions <typing:type expression>`. As such, the
159+
:external+typing:token:`~expression-grammar:type_expression` production will
160+
need to be updated to include the inlined syntax:
124161

125-
* The definitions of the fields in the inlined typed dictionary syntax
162+
.. productionlist:: inlined-typed-dictionaries-grammar
163+
new-type_expression: `~expression-grammar:type_expression`
164+
: | <TypedDict> '[' '{' (string: ':' `~expression-grammar:annotation_expression` ',')* '}' ']'
165+
(where string is any string literal)
126166

127167

128168
Backwards Compatibility
@@ -156,11 +196,9 @@ Mypy supports a similar syntax as an :option:`experimental feature <mypy:mypy.--
156196
def test_values() -> {"int": int, "str": str}:
157197
return {"int": 42, "str": "test"}
158198

159-
Pyright has added support in version `1.1.297`_ (using :class:`dict`), but was later
160-
removed in version `1.1.366`_.
199+
Pyright added support for the new syntax in version `1.1.387`_.
161200

162-
.. _1.1.297: https://github.com/microsoft/pyright/releases/tag/1.1.297
163-
.. _1.1.366: https://github.com/microsoft/pyright/releases/tag/1.1.366
201+
.. _1.1.387: https://github.com/microsoft/pyright/releases/tag/1.1.387
164202

165203
Runtime implementation
166204
----------------------
@@ -203,6 +241,24 @@ While this would avoid having to import :class:`~typing.TypedDict` from
203241
* If future work extends what inlined typed dictionaries can do, we don't have
204242
to worry about impact of sharing the symbol with :class:`dict`.
205243

244+
Using a simple dictionary
245+
-------------------------
246+
247+
Instead of subscripting the :class:`~typing.TypedDict` class, a plain
248+
dictionary could be used as an annotation::
249+
250+
def get_movie() -> {'title': str}: ...
251+
252+
However, :pep:`584` added union operators on dictionaries and :pep:`604`
253+
introduced :ref:`union types <python:types-union>`. Both features make use of
254+
the :ref:`bitwise or (|) <python:bitwise>` operator, making the following use
255+
cases incompatible, especially for runtime introspection::
256+
257+
# Dictionaries are merged:
258+
def fn() -> {'a': int} | {'b': str}: ...
259+
260+
# Raises a type error at runtime:
261+
def fn() -> {'a': int} | int: ...
206262

207263
Open Issues
208264
===========
@@ -220,6 +276,24 @@ Should we allow the following?::
220276
class SubTD(InlinedTD):
221277
pass
222278

279+
Using ``typing.Dict`` with a single argument
280+
--------------------------------------------
281+
282+
While using :class:`dict` isn't ideal, we could make use of
283+
:class:`typing.Dict` with a single argument::
284+
285+
def get_movie() -> Dict[{'title': str}]: ...
286+
287+
It is less verbose, doesn't have the baggage of :class:`dict`,
288+
and is defined as some kind of special form (an alias to the built-in
289+
``dict``).
290+
291+
However, it is currently marked as deprecated (although not scheduled for
292+
removal), so it might be confusing to undeprecate it.
293+
294+
This would also set a precedent on typing constructs being parametrizable
295+
with a different number of type arguments.
296+
223297

224298
Copyright
225299
=========

0 commit comments

Comments
 (0)