PEP 9999 – Inline type expressions and inline typed dictionaries
- Author:
- Victorien Plot <contact at vctrn.dev>
- Created:
- 30-Aug<-2025
- Status:
- Draft
- Type:
- Standards Track
Table of Contents
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.
It is also currently not possible to dynamically create new typed dictionary classes from existing ones, to change the value type and/or associated type qualifiers of keys.
This PEP proposes a new syntax in the language, to create inline typed dictionaries, that can be constructed using a comprehension syntax:
type Movie = <{'name': str, 'location': <{'city': str}>}>
type PartialMovie = <{K: NotRequired[ValueOf[Movie, K]] for K in KeyOf[Movie]}>
While introducing a new syntax specifically for type expressions (and more precisely
typed dictionaries) might be considered not persuasive enough, the use of the <
and >
characters partially mitigates this concern. This PEP introduces the concept of
inline type expressions, inline typed dictionaries being a single variant of this expression.
In the future, this syntax could be extended e.g. to define inline tuple expressions:
type TupleInt = tuple[int, int]
type TupleListInt = <(list[T] for T in TupleInt)>
New special forms (namely KeyOf
and ValueOf
) are introduced,
and used by the new inline syntax (although can be used separately as well).
Specification
Note
This section describes the specification of this PEP for the Python type system. See the runtime implementation section below for changes on the Python runtime.
Key specification
A key specification is a type that represents keys for a typed dictionary. The typing system already has types that are valid key specifications:
# Represents the keys 'a' and 'b':
Literal['a', 'b']
# Represents the keys of an "open" typed dictionary
# (i.e. a typed dictionary allowing extra items):
str
A new KeyOf
special form is introduced to the typing system, which is a
valid key specification. It takes a single type argument that must be a TypedDict
class, or a type variable bound to a TypedDict
class.
- If the type argument is a
TypedDict
class (sayTD
), the equivalence ofKeyOf[TD]
to other types depends on the closed specification of the typed dictionary:class TD(TypedDict): a: str b: NotRequired[str] assert_type(KeyOf[TD], Literal['a', 'b']) class TDOpen(TypedDict, extra_items=int): a: str assert_type(KeyOf[TDOpen], str)
- If the type argument is a type variable
T
(bound toTypedDict
),KeyOf[T]
can be seen as a type variable bound tostr
and is called a “boundKeyOf
specification”. ParameterizingKeyOf[T]
with a typed dictionary classTD
is equivalent toKeyOf[TD]
:def random_key_of[T: TypedDict](d: T) -> KeyOf[T]: return list(d.keys())[random.randrange(len(d))] class TD(TypedDict): a: str b: NotRequired[str] d: TD = {'a': 'value'} assert_type(random_key_of(d), Literal['a', 'b'])
Operations on key specifications
Two type-level operations can be applied on key specifications:
- Removing keys from a key specification, using the
-
operator:assert_type(Literal['a', 'b'] - Literal['b'], Literal['a']) assert_type(Literal['a', 'b'] - Literal['c'], Literal['a', 'b']) class TD(TypedDict): a: str b: NotRequired[str] assert_type(KeyOf[TD] - Literal['b'], Literal['a'])
A key specification that results in an empty set of keys is equivalent to
Never
:assert_type(Literal['a'] - Literal['a'], Never)
- Adding keys to a key specification, using the
+
operator:assert_type(Literal['a'] + Literal['b'], Literal['a', 'b'])
When applying such operations on bound KeyOf specifications, the evaluation of the type is deferred until it is parameterized:
def keys_minus_a[T: TypedDict](d: T) -> KeyOf[T] - Literal['a']:
...
class TD(TypedDict):
a: str
b: NotRequired[str]
d: TD = {'a': 'value'}
assert_type(keys_minus_a(d), Literal['b'])
Key specification views
A key specification view represents a single key in a key specification. It can appear only in a specific context: inline typed dictionaries with the comprehension syntax, described below.
The ValueOf
special form
A new ValueOf
special form is introduced to the typing system. It must
be parameterized with two parameters: a TypedDict
class or a type variable
bound to a TypedDict
class, and a key specification
view.
ValueOf[..., ...]
represents the value type of a typed dictionary item. Note that
type qualifiers aren’t represented by this special form
(these are only carried by the key specification view, as described in the
comprehension syntax section below).
Inline type expressions declarations
Inline type expressions are enclosed by the ‘Less-Than Sign’ (<
, U+003C) and
‘Greater-Than Sign’ (>
, U+003E) characters, and must contain an inner
expression:
def func(arg: <...>):
pass
Inline type expressions are a new form of type expression.
Inline typed dictionaries
An inline typed dictionary is a variant of an inline type expression. It is defined using
a new display syntax, similar to the existing dictionary display.
The ‘Left Curly Bracket’ ({
, U+007B) and ‘Right Curly Bracket’ (}
, U+007D)
characters are used, respectively after the <
and before the >
characters:
def func(arg: <{...}>):
pass
Two different syntaxes can be used, described in the following sections.
Simple display syntax
This syntax follows the same semantics as the functional syntax (the keys are strings representing the field names, and values are valid annotation expressions), and allows defining typed dictionaries “statically”:
type Movie = <{'name': str, 'location': <{'city': str}>}>
Although it is not possible to specify class arguments such as total
(the comprehension syntax aims to solve that),
any type qualifier can be used for indiviual fields:
type Movie = <{'name': NotRequired[str], 'year': ReadOnly[int]}>
Comprehension syntax
The comprehension syntax allows creating typed dictionary types dynamically. It is inspired from the existing dictionary comprehension syntax, with some simplifications. The general syntax is as follows:
type ComprTD = <{K: _type_expr_ for K in _key_spec_}>
Conceptually, the comprehension syntax enables the possibility to express a new typed dictionary type by mapping each key from a key specification to a specific value type, while preserving (and potentially altering) the qualifiers of such key.
By iterating [1] over the key specification using the for
clause, a key specification
view is created (in the given example, K
is a key specification view).
It carries the following information about a key:
- The key name.
- The associated value type.
- The associated type qualifiers.
These three informations are mapped to the newly created typed dictionary type, possibly with some modifications:
- The key name cannot be changed (e.g. it isn’t possible to add a string suffix). In the
general syntax example,
K
must be specified as is in the first expression on the left side of the colon. - The associated value type can be changed. Any valid annotation expression can
be used (e.g.
int
,Annotated[str, ...]
). If the key specification iterated over is aKeyOf
special form (and as such, brings aTypedDict
class or a type variableT
bound toTypedDict
in scope), the original value type can be accessed using theValueOf
special form.
- The associated type qualifiers are carried over, and
can be overridden for
Required
andNotRequired
(ReadOnly
is always carried over), by wrapping the associated value type inside the desired type qualifiers.
Here are some examples demonstrating these rules:
- Standalone type:
Comprehension syntax type Standalone = <{K: int for K in Literal['a', 'b']}>
Class equivalent class Standalone(TypedDict): a: int b: int
- Invalid modifications on keys:
Comprehension syntax type InvalidKeysAltering = <{K + '_suffix': int for K in Literal['a', 'b']}>
Class equivalent N/A Notes Must raise a type checker error - Changing the value type to
str
:Comprehension syntax class TD(TypedDict): a: int type TDAsStr = <{K: str for K in KeyOf[TD]}>
Class equivalent class TDAsStr(TypedDict): a: str
- Changing the value type to
str
, making the key not required:Comprehension syntax class TD(TypedDict): a: int type TDAsNotRequiredStr = <{K: NotRequired[str] for K in KeyOf[TD]}>
Class equivalent class TDAsNotRequiredStr(TypedDict): a: NotRequired[str]
Notes The NotRequired
type qualifier overrides the original ones (in this case, we can assumeRequired
is an implicit qualifier ona
). - Making all keys not required, keeping the value type:
Comprehension syntax class TD(TypedDict): a: int type TDAsNotRequired = <{K: NotRequired[ValueOf[TD, K]] for K in KeyOf[TD]}>
Class equivalent class TDAsNotRequired(TypedDict): a: NotRequired[int]
Notes The NotRequired
type qualifier overrides the original ones (in this case, we can assumeRequired
is an implicit qualifier ona
). - Making all keys read only, wrapping the value type inside
list
:Comprehension syntax class TD(TypedDict): a: NotRequired[int] type TDAsReadOnlyList = <{K: ReadOnly[list[ValueOf[TD, K]]] for K in KeyOf[TD]}>
Class equivalent class TDAsReadOnlyList(TypedDict): a: ReadOnly[NotRequired[list[int]]]
Notes Notice that NotRequired
is carried over, even ifValueOf[TD, K]
is mapped in a “nested” way. - Using a type alias to make every key not required:
Comprehension syntax type Partial[T: TypedDict] = <{K: NotRequired[ValueOf[TD, K]] for K in KeyOf[T]}>
Class equivalent Not expressible - Using a type alias to select only certain keys:
Comprehension syntax type Omit[T: TypedDict, K: KeyOf[T]] = <{P: ValueOf[T, P] for P in KeyOf[T] - K}>
Class equivalent Not expressible
While similar to the existing dictionary comprehension syntax, this syntax is defined separately, and the following differences can be found:
- Only a single
for
clause can be used. It must iterate over a key specification, and the key specification view variable used for the iteration must be used as the key (the first expression on the left side of the colon). - Unlike the dictionary comprehension syntax, it is not possible to use an
if
clause.
Typing specification changes
The type_expression
production will
be updated to include the KeyOf
and ValueOf
special forms,
and the inline syntax:
new-type_expression ::=type_expression
| <KeyOf> '[' name ']' (where name refers to an in-scope TypedDict or type variable bound to a TypedDict) | <ValueOf> '[' name ',' view ']' (where name refers to an in-scope TypedDict or a type variable bound to a TypedDict, and view refers to a key specification view) |inline_type_expression
inline_type_expression ::= '<'inline_typed_dict
'>' inline_typed_dict ::= '{' (string ':'annotation_expression
',')* '}' (where string is any string literal) | '{' K ':'annotation_expression
'for' 'K' 'in'type_expression
'}' (thetype_expression
on which the 'for' clause is applied must be a key specification)
Runtime implementation
Grammar changes
Inline type expressions are defined as an atom expression,
more specifically as an enclosure
:
new-enclosure ::=enclosure
|inline_type_expr_display
inline_type_expr_display ::= "<" "..." ">"
The grammar will define how the inner expression will be parsed (currently denoted
as "..."
) [2], and can produce different AST nodes:
inline_type_expr_display ::= "<"inline_typed_dict
">" inline_typed_dict ::= "{" [inline_typed_dict_item_list
|inline_typed_dict_comprehension
] "}" inline_typed_dict_item_list ::=inline_typed_dict_item
(","inline_typed_dict_item
)* [","] inline_typed_dict_item ::=stringliteral
":"expression
inline_typed_dict_comprehension ::=identifier
":"expression
"for"identifier
"in"expression
Footnotes
Source: https://github.com/python/peps/blob/main/peps/pep-9999.rst
Last modified: 2025-08-31 12:07:25 GMT