A RetroSearch Logo

Home - News ( United States | United Kingdom | Italy | Germany ) - Football scores

Search Query:

Showing content from https://python.github.io/peps/pep-0728/ below:

PEP 728 – TypedDict with Typed Extra Items

PEP 728 – TypedDict with Typed Extra Items
Author:
Zixuan James Li <p359101898 at gmail.com>
Sponsor:
Jelle Zijlstra <jelle.zijlstra at gmail.com>
Discussions-To:
Discourse thread
Status:
Accepted
Type:
Standards Track
Topic:
Typing
Created:
12-Sep-2023
Python-Version:
3.15
Post-History:
09-Feb-2024
Resolution:
15-Aug-2025
Table of Contents Abstract

This PEP adds two class parameters, closed and extra_items to type the extra items on a TypedDict. This addresses the need to define closed TypedDict types or to type a subset of keys that might appear in a dict while permitting additional items of a specified type.

Motivation

A typing.TypedDict type can annotate the value type of each known item in a dictionary. However, due to structural assignability, a TypedDict can have extra items that are not visible through its type. There is currently no way to restrict the types of items that might be present in the TypedDict type’s consistent subtypes.

Support Additional Keys for Unpack

PEP 692 adds a way to precisely annotate the types of individual keyword arguments represented by **kwargs using TypedDict with Unpack. However, because TypedDict cannot be defined to accept arbitrary extra items, it is not possible to allow additional keyword arguments that are not known at the time the TypedDict is defined.

Given the usage of pre-PEP 692 type annotation for **kwargs in existing codebases, it will be valuable to accept and type extra items on TypedDict so that the old typing behavior can be supported in combination with Unpack.

Previous Discussions

The new features introduced in this PEP would address several long-standing feature requests in the type system. Previous discussions include:

Rationale

Suppose we want a type that allows extra items of type str on a TypedDict.

Index Signatures in TypeScript allow this:

type Foo = {
    a: string
    [key: string]: string
}

This proposal aims to support a similar feature without syntax changes, offering a natural extension to the existing assignability rules.

We propose to add a class parameter extra_items to TypedDict. It accepts a type expression as the argument; when it is present, extra items are allowed, and their value types must be assignable to the type expression value.

An application of this is to disallow extra items. We propose to add a closed class parameter, which only accepts a literal True or False as the argument. It should be a runtime error when closed and extra_items are used at the same time.

Different from index signatures, the types of the known items do not need to be assignable to the extra_items argument.

There are some advantages to this approach:

Specification

This specification is structured to parallel PEP 589 to highlight changes to the original TypedDict specification.

If extra_items is specified, extra items are treated as non-required items matching the extra_items argument, whose keys are allowed when determining supported and unsupported operations.

The closed Class Parameter

When neither extra_items nor closed=True is specified, closed=False is assumed. The TypedDict should allow non-required extra items of value type ReadOnly[object] during inheritance or assignability checks, to preserve the default TypedDict behavior. Extra keys included in TypedDict object construction should still be caught, as mentioned in TypedDict’s typing spec.

When closed=True is set, no extra items are allowed. This is equivalent to extra_items=Never, because there can’t be a value type that is assignable to Never. It is a runtime error to use the closed and extra_items parameters in the same TypedDict definition.

Similar to total, only a literal True or False is supported as the value of the closed argument. Type checkers should reject any non-literal value.

Passing closed=False explicitly requests the default TypedDict behavior, where arbitrary other keys may be present and subclasses may add arbitrary items. It is a type checker error to pass closed=False if a superclass has closed=True or sets extra_items.

If closed is not provided, the behavior is inherited from the superclass. If the superclass is TypedDict itself or the superclass does not have closed=True or the extra_items parameter, the previous TypedDict behavior is preserved: arbitrary extra items are allowed. If the superclass has closed=True, the child class is also closed:

class BaseMovie(TypedDict, closed=True):
    name: str

class MovieA(BaseMovie):  # OK, still closed
    pass

class MovieB(BaseMovie, closed=True):  # OK, but redundant
    pass

class MovieC(BaseMovie, closed=False):  # Type checker error
    pass

As a consequence of closed=True being equivalent to extra_items=Never, the same rules that apply to extra_items=Never also apply to closed=True. While they both have the same effect, closed=True is preferred over extra_items=Never.

It is possible to use closed=True when subclassing if the extra_items argument is a read-only type:

class Movie(TypedDict, extra_items=ReadOnly[str]):
    pass

class MovieClosed(Movie, closed=True):  # OK
    pass

class MovieNever(Movie, extra_items=Never):  # OK, but 'closed=True' is preferred
    pass

This will be further discussed in a later section.

closed is also supported with the functional syntax:

Movie = TypedDict("Movie", {"name": str}, closed=True)
Interaction with Totality

It is an error to use Required[] or NotRequired[] with extra_items. total=False and total=True have no effect on extra_items itself.

The extra items are non-required, regardless of the totality of the TypedDict. Operations that are available to NotRequired items should also be available to the extra items:

class Movie(TypedDict, extra_items=int):
    name: str

def f(movie: Movie) -> None:
    del movie["name"]  # Not OK. The value type of 'name' is 'Required[int]'
    del movie["year"]  # OK. The value type of 'year' is 'NotRequired[int]'
Interaction with Unpack

For type checking purposes, Unpack[SomeTypedDict] with extra items should be treated as its equivalent in regular parameters, and the existing rules for function parameters still apply:

class MovieNoExtra(TypedDict):
    name: str

class MovieExtra(TypedDict, extra_items=int):
    name: str

def f(**kwargs: Unpack[MovieNoExtra]) -> None: ...
def g(**kwargs: Unpack[MovieExtra]) -> None: ...

# Should be equivalent to:
def f(*, name: str) -> None: ...
def g(*, name: str, **kwargs: int) -> None: ...

f(name="No Country for Old Men", year=2007) # Not OK. Unrecognized item
g(name="No Country for Old Men", year=2007) # OK
Interaction with Read-only Items

When the extra_items argument is annotated with the ReadOnly[] type qualifier, the extra items on the TypedDict have the properties of read-only items. This interacts with inheritance rules specified in Read-only Items.

Notably, if the TypedDict type specifies extra_items to be read-only, subclasses of the TypedDict type may redeclare extra_items.

Because a non-closed TypedDict type implicitly allows non-required extra items of value type ReadOnly[object], its subclass can override the extra_items argument with more specific types.

More details are discussed in the later sections.

Inheritance

extra_items is inherited in a similar way as a regular key: value_type item. As with the other keys, the inheritance rules and Read-only Items inheritance rules apply.

We need to reinterpret these rules to define how extra_items interacts with them.

First, it is not allowed to change the value of extra_items in a subclass unless it is declared to be ReadOnly in the superclass:

class Parent(TypedDict, extra_items=int | None):
    pass

class Child(Parent, extra_items=int): # Not OK. Like any other TypedDict item, extra_items's type cannot be changed
    pass

Second, extra_items=T effectively defines the value type of any unnamed items accepted to the TypedDict and marks them as non-required. Thus, the above restriction applies to any additional items defined in a subclass. For each item added in a subclass, all of the following conditions should apply:

For example:

class MovieBase(TypedDict, extra_items=int | None):
    name: str

class MovieRequiredYear(MovieBase):  # Not OK. Required key 'year' is not known to 'MovieBase'
    year: int | None

class MovieNotRequiredYear(MovieBase):  # Not OK. 'int | None' is not consistent with 'int'
    year: NotRequired[int]

class MovieWithYear(MovieBase):  # OK
    year: NotRequired[int | None]

class BookBase(TypedDict, extra_items=ReadOnly[int | str]):
    title: str

class Book(BookBase, extra_items=str):  # OK
    year: int  # OK

An important side effect of the inheritance rules is that we can define a TypedDict type that disallows additional items:

class MovieClosed(TypedDict, extra_items=Never):
    name: str

Here, passing the value Never to extra_items specifies that there can be no other keys in MovieFinal other than the known ones. Because of its potential common use, there is a preferred alternative:

class MovieClosed(TypedDict, closed=True):
    name: str

where we implicitly assume that extra_items=Never.

Assignability

Let S be the set of keys of the explicitly defined items on a TypedDict type. If it specifies extra_items=T, the TypedDict type is considered to have an infinite set of items that all satisfy the following conditions.

For type checking purposes, let extra_items be a non-required pseudo-item when checking for assignability according to rules defined in the Read-only Items section, with a new rule added in bold text as follows:

A TypedDict type

B

is

assignable

to a TypedDict type

A

if

B

is

structurally

assignable to

A

. This is true if and only if all of the following are satisfied:

The following examples illustrate these checks in action.

extra_items puts various restrictions on additional items for assignability checks:

class Movie(TypedDict, extra_items=int | None):
    name: str

class MovieDetails(TypedDict, extra_items=int | None):
    name: str
    year: NotRequired[int]

details: MovieDetails = {"name": "Kill Bill Vol. 1", "year": 2003}
movie: Movie = details  # Not OK. While 'int' is assignable to 'int | None',
                        # 'int | None' is not assignable to 'int'

class MovieWithYear(TypedDict, extra_items=int | None):
    name: str
    year: int | None

details: MovieWithYear = {"name": "Kill Bill Vol. 1", "year": 2003}
movie: Movie = details  # Not OK. 'year' is not required in 'Movie',
                        # but it is required in 'MovieWithYear'

where MovieWithYear (B) is not assignable to Movie (A) according to this rule:

When extra_items is specified to be read-only on a TypedDict type, it is possible for an item to have a narrower type than the extra_items argument:

class Movie(TypedDict, extra_items=ReadOnly[str | int]):
    name: str

class MovieDetails(TypedDict, extra_items=int):
    name: str
    year: NotRequired[int]

details: MovieDetails = {"name": "Kill Bill Vol. 2", "year": 2004}
movie: Movie = details  # OK. 'int' is assignable to 'str | int'.

This behaves the same way as if year: ReadOnly[str | int] is an item explicitly defined in Movie.

extra_items as a pseudo-item follows the same rules that other items have, so when both TypedDicts types specify extra_items, this check is naturally enforced:

class MovieExtraInt(TypedDict, extra_items=int):
    name: str

class MovieExtraStr(TypedDict, extra_items=str):
    name: str

extra_int: MovieExtraInt = {"name": "No Country for Old Men", "year": 2007}
extra_str: MovieExtraStr = {"name": "No Country for Old Men", "description": ""}
extra_int = extra_str  # Not OK. 'str' is not assignable to extra items type 'int'
extra_str = extra_int  # Not OK. 'int' is not assignable to extra items type 'str'

A non-closed TypedDict type implicitly allows non-required extra keys of value type ReadOnly[object]. Applying the assignability rules between this type and a closed TypedDict type is allowed:

class MovieNotClosed(TypedDict):
    name: str

extra_int: MovieExtraInt = {"name": "No Country for Old Men", "year": 2007}
not_closed: MovieNotClosed = {"name": "No Country for Old Men"}
extra_int = not_closed  # Not OK.
                        # 'extra_items=ReadOnly[object]' implicitly on 'MovieNotClosed'
                        # is not assignable to with 'extra_items=int'
not_closed = extra_int  # OK
Interaction with Constructors

TypedDicts that allow extra items of type T also allow arbitrary keyword arguments of this type when constructed by calling the class object:

class NonClosedMovie(TypedDict):
    name: str

NonClosedMovie(name="No Country for Old Men")  # OK
NonClosedMovie(name="No Country for Old Men", year=2007)  # Not OK. Unrecognized item

class ExtraMovie(TypedDict, extra_items=int):
    name: str

ExtraMovie(name="No Country for Old Men")  # OK
ExtraMovie(name="No Country for Old Men", year=2007)  # OK
ExtraMovie(
    name="No Country for Old Men",
    language="English",
)  # Not OK. Wrong type for extra item 'language'

# This implies 'extra_items=Never',
# so extra keyword arguments would produce an error
class ClosedMovie(TypedDict, closed=True):
    name: str

ClosedMovie(name="No Country for Old Men")  # OK
ClosedMovie(
    name="No Country for Old Men",
    year=2007,
)  # Not OK. Extra items not allowed
Supported and Unsupported Operations

This statement from the typing spec still holds true.

Operations with arbitrary str keys (instead of string literals or other expressions with known string values) should generally be rejected.

Operations that already apply to NotRequired items should generally also apply to extra items, following the same rationale from the typing spec:

The exact type checking rules are up to each type checker to decide. In some cases potentially unsafe operations may be accepted if the alternative is to generate false positive errors for idiomatic code.

Some operations, including indexed accesses and assignments with arbitrary str keys, may be allowed due to the TypedDict being assignable to Mapping[str, VT] or dict[str, VT]. The two following sections will expand on that.

Interaction with Mapping[str, VT]

A TypedDict type is assignable to a type of the form Mapping[str, VT] when all value types of the items in the TypedDict are assignable to VT. For the purpose of this rule, a TypedDict that does not have extra_items= or closed= set is considered to have an item with a value of type ReadOnly[object]. This extends the current assignability rule from the typing spec.

For example:

class MovieExtraStr(TypedDict, extra_items=str):
    name: str

extra_str: MovieExtraStr = {"name": "Blade Runner", "summary": ""}
str_mapping: Mapping[str, str] = extra_str  # OK

class MovieExtraInt(TypedDict, extra_items=int):
    name: str

extra_int: MovieExtraInt = {"name": "Blade Runner", "year": 1982}
int_mapping: Mapping[str, int] = extra_int  # Not OK. 'int | str' is not assignable with 'int'
int_str_mapping: Mapping[str, int | str] = extra_int  # OK

Type checkers should infer the precise signatures of values() and items() on such TypedDict types:

def foo(movie: MovieExtraInt) -> None:
    reveal_type(movie.items())  # Revealed type is 'dict_items[str, str | int]'
    reveal_type(movie.values())  # Revealed type is 'dict_values[str, str | int]'

By extension of this assignability rule, type checkers may allow indexed accesses with arbitrary str keys when extra_items or closed=True is specified. For example:

def bar(movie: MovieExtraInt, key: str) -> None:
    reveal_type(movie[key])  # Revealed type is 'str | int'

Defining the type narrowing behavior for TypedDict is out-of-scope for this PEP. This leaves flexibility for a type checker to be more/less restrictive about indexed accesses with arbitrary str keys. For example, a type checker may opt for more restriction by requiring an explicit 'x' in d check.

Interaction with dict[str, VT]

Because the presence of extra_items on a closed TypedDict type prohibits additional required keys in its structural subtypes, we can determine if the TypedDict type and its structural subtypes will ever have any required key during static analysis.

The TypedDict type is assignable to dict[str, VT] if all items on the TypedDict type satisfy the following conditions:

For example:

class IntDict(TypedDict, extra_items=int):
    pass

class IntDictWithNum(IntDict):
    num: NotRequired[int]

def f(x: IntDict) -> None:
    v: dict[str, int] = x  # OK
    v.clear()  # OK

not_required_num_dict: IntDictWithNum = {"num": 1, "bar": 2}
regular_dict: dict[str, int] = not_required_num_dict  # OK
f(not_required_num_dict)  # OK

In this case, methods that are previously unavailable on a TypedDict are allowed, with signatures matching dict[str, VT] (e.g.: __setitem__(self, key: str, value: VT) -> None):

not_required_num_dict.clear()  # OK

reveal_type(not_required_num_dict.popitem())  # OK. Revealed type is 'tuple[str, int]'

def f(not_required_num_dict: IntDictWithNum, key: str):
  not_required_num_dict[key] = 42  # OK
  del not_required_num_dict[key]  # OK

Notes on indexed accesses from the previous section still apply.

dict[str, VT] is not assignable to a TypedDict type, because such dict can be a subtype of dict:

class CustomDict(dict[str, int]):
    pass

def f(might_not_be_a_builtin_dict: dict[str, int]):
    int_dict: IntDict = might_not_be_a_builtin_dict # Not OK

not_a_builtin_dict: CustomDict = {"num": 1}
f(not_a_builtin_dict)
Runtime behavior

At runtime, it is an error to pass both the closed and extra_items arguments in the same TypedDict definition, whether using the class syntax or the functional syntax. For simplicity, the runtime does not check other invalid combinations involving inheritance.

For introspection, the closed and extra_items arguments are mapped to two new attributes on the resulting TypedDict object: __closed__ and __extra_items__. These attributes reflect exactly what was passed to the TypedDict constructor, without considering superclasses.

If closed is not passed, the value of __closed__ is None. If extra_items is not passed, the value of __extra_items__ is the new sentinel object typing.NoExtraItems. (It cannot be None, because extra_items=None is a valid definition that indicates all extra items must be None.)

How to Teach This

The new features introduced in this PEP can be taught together with the concept of inheritance as it applies to TypedDict. A possible outline could be:

The concept of a closed TypedDict should also be cross-referenced in documentation for related concepts. For example, type narrowing with the in operator works differently, perhaps more intuitively, with closed TypedDict types. In addition, when Unpack is used for keyword arguments, a closed TypedDict can be useful to restrict the allowed keyword arguments.

Backwards Compatibility

Because extra_items is an opt-in feature, no existing codebase will break due to this change.

Note that closed and extra_items as keyword arguments do not collide with other keys when using something like TD = TypedDict("TD", foo=str, bar=int), because this syntax has already been removed in Python 3.13.

Because this is a type-checking feature, it can be made available to older versions as long as the type checker supports it.

Rejected Ideas Use @final instead of closed Class Parameter

This was discussed here.

Quoting a relevant comment from Eric Traut:

The @final class decorator indicates that a class cannot be subclassed. This makes sense for classes that define nominal types. However, TypedDict is a structural type, similar to a Protocol. That means two TypedDict classes with different names but the same field definitions are equivalent types. Their names and hierarchies don’t matter for determining type consistency. For that reason, @final has no impact on a TypedDict type consistency rules, nor should it change the behavior of items or values.

Support a New Syntax of Specifying Keys

By introducing a new syntax that allows specifying string keys, we could deprecate the functional syntax of defining TypedDict types and address the key conflict issues if we decide to reserve a special key to type extra items.

For example:

class Foo(TypedDict):
    name: str  # Regular item
    _: bool    # Type of extra items
    __items__ = {
        "_": int,   # Literal "_" as a key
        "class": str,  # Keyword as a key
        "tricky.name?": float,  # Arbitrary str key
    }

This was proposed here by Jukka. The '_' key is chosen for not needing to invent a new name, and its similarity with the match statement.

This will allow us to deprecate the functional syntax of defining TypedDict types altogether, but there are some disadvantages. For example:

Reference Implementation

This is supported in pyright 1.1.386, and an earlier revision is supported in pyanalyze 0.12.0.

This is also supported in typing-extensions 4.13.0.

Acknowledgments

Thanks to Jelle Zijlstra for sponsoring this PEP and providing review feedback, Eric Traut who proposed the original design this PEP iterates on, and Alice Purcell for offering their perspective as the author of PEP 705.

Copyright

This 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