Skip to content

Commit c364510

Browse files
committed
Inlined typed dictionaries PEP
1 parent a5fb32d commit c364510

File tree

2 files changed

+219
-0
lines changed

2 files changed

+219
-0
lines changed

peps/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"typing": ("https://typing.readthedocs.io/en/latest/", None),
7171
"trio": ("https://trio.readthedocs.io/en/latest/", None),
7272
"devguide": ("https://devguide.python.org/", None),
73+
"mypy": ("https://mypy.readthedocs.io/en/latest/", None),
7374
"py3.11": ("https://docs.python.org/3.11/", None),
7475
"py3.12": ("https://docs.python.org/3.12/", None),
7576
"py3.13": ("https://docs.python.org/3.13/", None),

peps/pep-9995.rst

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
PEP: 9995
2+
Title: Inlined typed dictionaries
3+
Author: Victorien Plot <contact@vctrn.dev>
4+
Discussions-To:
5+
Status: Draft
6+
Type: Standards Track
7+
Topic: Typing
8+
Created: 23-Oct-2024
9+
Post-History:
10+
Python-Version: 3.14
11+
Resolution:
12+
13+
.. highlight:: python
14+
15+
16+
Abstract
17+
========
18+
19+
:pep:`589` defines a `class-based <https://typing.readthedocs.io/en/latest/spec/typeddict.html#class-based-syntax>`_ and a
20+
:ref:`functional syntax <typing:typeddict-functional-syntax>` to create typed
21+
dictionaries. In both scenarios, it requires defining a class or assigning to
22+
a value. In some situations, this can add unnecessary boilerplate, especially
23+
if the typed dictionary is only used once.
24+
25+
This PEP proposes the addition of a new inlined syntax, by subscripting the
26+
:class:`~typing.TypedDict` type::
27+
28+
from typing import TypedDict
29+
30+
def get_movie() -> TypedDict[{'name': str, 'year': int}]:
31+
return {
32+
'name': 'Blade Runner',
33+
'year': 1982,
34+
}
35+
36+
Motivation
37+
==========
38+
39+
Python dictionaries are an essential data structure of the language. Many
40+
times, it is used to return or accept structured data in functions. However,
41+
it can get tedious to define :class:`~typing.TypedDict` classes:
42+
43+
* A typed dictionary requires a name, which might not be relevant.
44+
* Nested dictionaries requires more than one class definition.
45+
46+
Taking a simple function returning some nested structured data as an example::
47+
48+
from typing import TypedDict
49+
50+
class ProductionCompany(TypedDict):
51+
name: str
52+
location: str
53+
54+
class Movie(TypedDict):
55+
name: str
56+
year: int
57+
production: ProductionCompany
58+
59+
60+
def get_movie() -> Movie:
61+
return {
62+
'name': 'Blade Runner',
63+
'year': 1982,
64+
'production': {
65+
'name': 'Warner Bros.',
66+
'location': 'California',
67+
}
68+
}
69+
70+
71+
Rationale
72+
=========
73+
74+
The new inlined syntax can be used to resolve these problems::
75+
76+
def get_movie() -> TypedDict[{'name': str, year: int, 'production': {'name': str, 'location': str}}]:
77+
...
78+
79+
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.
81+
82+
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::
84+
85+
type InlinedDict = TypedDict[{'name': str}]
86+
87+
Specification
88+
=============
89+
90+
The :class:`~typing.TypedDict` class is made subscriptable, and accepts a
91+
single type argument which must be a :class:`dict`, following the same
92+
semantics as the :ref:`functional syntax <typing:typeddict-functional-syntax>`
93+
(the dictionary keys are strings representing the field names, and values are
94+
valid :ref:`annotation expressions <typing:annotation-expression>`).
95+
96+
Inlined typed dictionaries can be referred as being *anonymous*, meaning they
97+
don't have a name. For this reason, their :attr:`~type.__name__` attribute
98+
will be set to an empty string.
99+
100+
It is not possible to specify any class arguments such as ``total``.
101+
102+
Runtime behavior
103+
----------------
104+
105+
Although :class:`~typing.TypedDict` is commonly referred as a class, it is
106+
implemented as a function at runtime. To be made subscriptable, it will be
107+
changed to be a class.
108+
109+
Creating an inlined typed dictionary results in a new class, so both syntaxes
110+
return the same type::
111+
112+
from typing import TypedDict
113+
114+
T1 = TypedDict('T1', {'a': int})
115+
T2 = TypedDict[{'a': int}]
116+
117+
118+
Backwards Compatibility
119+
=======================
120+
121+
Apart from the :class:`~typing.TypedDict` internal implementation change, this
122+
PEP does not bring any backwards incompatible changes.
123+
124+
125+
Security Implications
126+
=====================
127+
128+
There are no known security consequences arising from this PEP.
129+
130+
131+
How to Teach This
132+
=================
133+
134+
The new inlined syntax will be documented both in the :mod:`typing` module
135+
documentation and the :ref:`typing specification <typing:typed-dictionaries>`.
136+
137+
As mentioned in the `Rationale`_, it should be mentioned that inlined typed
138+
dictionaries should be used for small structured data to not hurt readability.
139+
140+
141+
Reference Implementation
142+
========================
143+
144+
Mypy supports a similar syntax as an :option:`experimental feature <mypy:mypy.--enable-incomplete-feature>`::
145+
146+
def test_values() -> {"int": int, "str": str}:
147+
return {"int": 42, "str": "test"}
148+
149+
Pyright has added support in version `1.1.297`_ (using :class:`dict`), but was later
150+
removed in version `1.1.366`_.
151+
152+
.. _1.1.297: https://github.com/microsoft/pyright/releases/tag/1.1.297
153+
.. _1.1.366: https://github.com/microsoft/pyright/releases/tag/1.1.366
154+
155+
Runtime implementation
156+
----------------------
157+
158+
A draft implementation is available `here <https://github.com/Viicos/cpython/commit/49e5a83f>`_.
159+
160+
161+
Rejected Ideas
162+
==============
163+
164+
Using the functional syntax in annotations
165+
------------------------------------------
166+
167+
The alternative functional syntax could be used as an annotation directly::
168+
169+
def get_movie() -> TypedDict('Movie', {'title': str}): ...
170+
171+
However, call expressions are currently unsupported in such a context for
172+
various reasons (expensive to process, evaluating them is not standardized).
173+
174+
This would also require a name which is sometimes not relevant.
175+
176+
Using ``dict`` with a single type argument
177+
------------------------------------------
178+
179+
We could reuse :class:`dict` with a single type argument to express the same
180+
concept::
181+
182+
def get_movie() -> dict[{'title': str}]: ...
183+
184+
While this would avoid having to import :class:`~typing.TypedDict` from
185+
:mod:`typing`, this solution has several downsides:
186+
187+
* For type checkers, :class:`dict` is a regular class with two type variables.
188+
Allowing :class:`dict` to be parametrized with a single type argument would
189+
require special casing from type checkers, as there is no way to express
190+
parametrization overloads. On ther other hand, :class:`~typing.TypedDict` is
191+
already a :term:`special form <typing:special form>`.
192+
193+
* If fufure work extends what inlined typed dictionaries can do, we don't have
194+
to worry about impact of sharing the symbol with :class:`dict`.
195+
196+
197+
Open Issues
198+
===========
199+
200+
Subclassing an inlined typed dictionary
201+
---------------------------------------
202+
203+
Should we allow the following?::
204+
205+
from typing import TypedDict
206+
207+
InlinedTD = TypedDict[{'a': int}]
208+
209+
210+
class SubTD(InlinedTD):
211+
pass
212+
213+
214+
Copyright
215+
=========
216+
217+
This document is placed in the public domain or under the
218+
CC0-1.0-Universal license, whichever is more permissive.

0 commit comments

Comments
 (0)