|
| 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