PEP 589 defines a class-based and a functional syntax to create typed dictionaries. In both scenarios, it requires defining a class or assigning to a value. In some situations, this can add unnecessary boilerplate, especially if the typed dictionary is only used once.
This PEP proposes the addition of a new inline syntax, by subscripting the TypedDict
type:
from typing import TypedDict def get_movie() -> TypedDict[{'name': str, 'year': int}]: return { 'name': 'Blade Runner', 'year': 1982, }Motivation
Python dictionaries are an essential data structure of the language. Many times, it is used to return or accept structured data in functions. However, it can get tedious to define TypedDict
classes:
Taking a simple function returning some nested structured data as an example:
from typing import TypedDict class ProductionCompany(TypedDict): name: str location: str class Movie(TypedDict): name: str year: int production: ProductionCompany def get_movie() -> Movie: return { 'name': 'Blade Runner', 'year': 1982, 'production': { 'name': 'Warner Bros.', 'location': 'California', } }Rationale
The new inline syntax can be used to resolve these problems:
def get_movie() -> TypedDict[{'name': str, 'year': int, 'production': TypedDict[{'name': str, 'location': str}]}]: ...
While less useful (as the functional or even the class-based syntax can be used), inline typed dictionaries can be assigned to a variable, as an alias:
InlineTD = TypedDict[{'name': str}] def get_movie() -> InlineTD: ...Specification
The TypedDict
special form is made subscriptable, and accepts a single type argument which must be a dict
, following the same semantics as the functional syntax (the dictionary keys are strings representing the field names, and values are valid annotation expressions). Only the comma-separated list of key: value
pairs within braces constructor ({k: <type>}
) is allowed, and should be specified directly as the type argument (i.e. it is not allowed to use a variable which was previously assigned a dict
instance).
Inline typed dictionaries can be referred to as anonymous, meaning they don’t have a specific name (see the runtime behavior section).
It is possible to define a nested inline dictionary:
Movie = TypedDict[{'name': str, 'production': TypedDict[{'location': str}]}] # Note that the following is invalid as per the updated `type_expression` grammar: Movie = TypedDict[{'name': str, 'production': {'location': str}}]
Although it is not possible to specify any class arguments such as total
, any type qualifier can be used for individual fields:
Movie = TypedDict[{'name': NotRequired[str], 'year': ReadOnly[int]}]
Inline typed dictionaries are implicitly total, meaning all keys must be present. Using the Required
type qualifier is thus redundant.
Type variables are allowed in inline typed dictionaries, provided that they are bound to some outer scope:
class C[T]: inline_td: TypedDict[{'name': T}] # OK, `T` is scoped to the class `C`. reveal_type(C[int]().inline_td['name']) # Revealed type is 'int' def fn[T](arg: T) -> TypedDict[{'name': T}]: ... # OK: `T` is scoped to the function `fn`. reveal_type(fn('a')['name']) # Revealed type is 'str' type InlineTD[T] = TypedDict[{'name': T}] # OK, `T` is scoped to the type alias. T = TypeVar('T') InlineTD = TypedDict[{'name': T}] # OK, same as the previous type alias, but using the old-style syntax. def func(): InlineTD = TypedDict[{'name': T}] # Not OK: `T` refers to a type variable that is not bound to the scope of `func`.
Inline typed dictionaries can be extended:
InlineTD = TypedDict[{'a': int}] class SubTD(InlineTD): passTyping specification changes
The inline typed dictionary adds a new kind of type expression. As such, the type_expression
production will be updated to include the inline syntax:
new-type_expression ::=Runtime behaviortype_expression
| <TypedDict> '[' '{' (string: ':'annotation_expression
',')* '}' ']' (where string is any string literal)
Creating an inline typed dictionary results in a new class, so T1
and T2
are of the same type:
from typing import TypedDict T1 = TypedDict('T1', {'a': int}) T2 = TypedDict[{'a': int}]
As inline typed dictionaries are meant to be anonymous, their __name__
attribute will be set to the <inline TypedDict>
string literal. In the future, an explicit class attribute could be added to make them distinguishable from named classes.
Although TypedDict
is documented as a class, the way it is defined is an implementation detail. The implementation will have to be tweaked so that TypedDict
can be made subscriptable.
This PEP does not bring any backwards incompatible changes.
Security ImplicationsThere are no known security consequences arising from this PEP.
How to Teach ThisThe new inline syntax will be documented both in the typing
module documentation and the typing specification.
When complex dictionary structures are used, having everything defined on a single line can hurt readability. Code formatters can help by formatting the inline type dictionary across multiple lines:
def edit_movie( movie: TypedDict[{ 'name': str, 'year': int, 'production': TypedDict[{ 'location': str, }], }], ) -> None: ...Reference Implementation
Mypy supports a similar syntax as an experimental feature
:
def test_values() -> {"int": int, "str": str}: return {"int": 42, "str": "test"}
Support for this PEP is added in this pull request.
Pyright added support for the new syntax in version 1.1.387.
Runtime implementationThe necessary changes were first implemented in typing_extensions in this pull request.
Rejected Ideas Using the functional syntax in annotationsThe alternative functional syntax could be used as an annotation directly:
def get_movie() -> TypedDict('Movie', {'title': str}): ...
However, call expressions are currently unsupported in such a context for various reasons (expensive to process, evaluating them is not standardized).
This would also require a name which is sometimes not relevant.
Usingdict
or typing.Dict
with a single type argument
We could reuse dict
or typing.Dict
with a single type argument to express the same concept:
def get_movie() -> dict[{'title': str}]: ...
While this would avoid having to import TypedDict
from typing
, this solution has several downsides:
dict
is a regular class with two type variables. Allowing dict
to be parametrized with a single type argument would require special casing from type checkers, as there is no way to express parametrization overloads. On the other hand, TypedDict
is already a special form.dict
.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).Instead of subscripting the TypedDict
class, a plain dictionary could be used as an annotation:
def get_movie() -> {'title': str}: ...
However, PEP 584 added union operators on dictionaries and PEP 604 introduced union types. Both features make use of the bitwise or (|) operator, making the following use cases incompatible, especially for runtime introspection:
# Dictionaries are merged: def fn() -> {'a': int} | {'b': str}: ... # Raises a type error at runtime: def fn() -> {'a': int} | int: ...Extending other typed dictionaries
Several syntaxes could be used to have the ability to extend other typed dictionaries:
InlineBase = TypedDict[{'a': int}] Inline = TypedDict[InlineBase, {'b': int}] # or, by providing a slice: Inline = TypedDict[{'b': int} : (InlineBase,)]
As inline 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 could cover this use case.
Open Issues Inline typed dictionaries and extra itemsPEP 728 introduces the concept of closed type dictionaries. If this PEP were to be accepted, inline typed dictionaries will be closed by default. This means PEP 728 needs to be addressed first, so that this PEP can be updated accordingly.
CopyrightThis document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4