Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

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 (say TD), the equivalence of KeyOf[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 to TypedDict), KeyOf[T] can be seen as a type variable bound to str and is called a “bound KeyOf specification”. Parameterizing KeyOf[T] with a typed dictionary class TD is equivalent to KeyOf[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:

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 a KeyOf special form (and as such, brings a TypedDict class or a type variable T bound to TypedDict in scope), the original value type can be accessed using the ValueOf special form.
  • The associated type qualifiers are carried over, and can be overridden for Required and NotRequired (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 assume Required is an implicit qualifier on a).
  • 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 assume Required is an implicit qualifier on a).
  • 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 if ValueOf[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 '}'
                                  (the type_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