PEP 764 – Inlined typed dictionaries
- Author:
- Victorien Plot <contact at vctrn.dev>
- Sponsor:
- Eric Traut <erictr at microsoft.com>
- Status:
- Draft
- Type:
- Standards Track
- Topic:
- Typing
- Created:
- 25-Oct-2024
- Python-Version:
- 3.14
Abstract
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 inlined 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:
- A typed dictionary requires a name, which might not be relevant.
- Nested dictionaries require more than one class definition.
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 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:
InlinedTD = TypedDict[{'name': str}]
def get_movie() -> InlinedTD:
...
Specification
The TypedDict
class 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).
Inlined typed dictionaries can be referred to as anonymous, meaning they don’t have a name (see the runtime behavior section).
It is possible to define a nested inlined 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]}]
Inlined typed dictionaries are implicitly total, meaning all keys must be
present. Using the Required
type qualifier is thus redundant.
Type variables are allowed in inlined typed dictionaries, provided that they are bound to some outer scope:
class C[T]:
inlined_td: TypedDict[{'name': T}] # OK, `T` is scoped to the class `C`.
reveal_type(C[int]().inlined_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 InlinedTD[T] = TypedDict[{'name': T}] # OK, `T` is scoped to the type alias.
T = TypeVar('T')
InlinedTD = TypedDict[{'name': T}] # Not OK, `T` refers to a type variable that is not bound to any scope.
Typing specification changes
The inlined typed dictionary adds a new kind of
type expression. As such, the
type_expression
production will
be updated to include the inlined syntax:
new-type_expression ::=type_expression
| <TypedDict> '[' '{' (string: ':'annotation_expression
',')* '}' ']' (where string is any string literal)
Runtime behavior
Although TypedDict
is commonly referred as a class, it is
implemented as a function at runtime. To be made subscriptable, it will be
changed to be a class.
Creating an inlined 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 inlined typed dictionaries are are meant to be anonymous, their
__name__
attribute will be set to an empty string.
Backwards Compatibility
This PEP does not bring any backwards incompatible changes.
Security Implications
There are no known security consequences arising from this PEP.
How to Teach This
The new inlined syntax will be documented both in the typing
module
documentation and the 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.
Reference Implementation
Mypy supports a similar syntax as an experimental feature
:
def test_values() -> {"int": int, "str": str}:
return {"int": 42, "str": "test"}
Pyright added support for the new syntax in version 1.1.387.
Runtime implementation
A draft implementation is available here.
Rejected Ideas
Using the functional syntax in annotations
The 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.
Using dict
with a single type argument
We could reuse 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:
- For type checkers,
dict
is a regular class with two type variables. Allowingdict
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. - If future work extends what inlined typed dictionaries can do, we don’t have
to worry about impact of sharing the symbol with
dict
.
Using a simple dictionary
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: ...
Open Issues
Subclassing an inlined typed dictionary
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?:
InlinedBase = TypedDict[{'a': int}]
Inlined = TypedDict[InlinedBase, {'b': int}]
Using typing.Dict
with a single argument
While using dict
isn’t ideal, we could make use of
typing.Dict
with a single argument:
def get_movie() -> Dict[{'title': str}]: ...
It is less verbose, doesn’t have the baggage of 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.
Should inlined typed dictionaries be proper classes?
The PEP currently defines inlined typed dictionaries as type objects, to be in
line with the existing syntaxes. To work around the fact that they don’t have
a name, their __name__
attribute is set to an empty string.
This is somewhat arbitrary, and an alternative name could be used as well
(e.g. '<TypedDict>'
).
Alternatively, inlined typed dictionaries could be defined as instances of a
new (internal) typing class, e.g. typing._InlinedTypedDict
. While
this solves the naming issue, it requires extra logic in the runtime
implementation to provide the introspection attributes (such as
__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
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.
Source: https://github.com/python/peps/blob/main/peps/pep-0764.rst
Last modified: 2025-01-29 04:53:16 GMT