extra_items
Class Parameterclosed
Class ParameterUnpack
@final
instead of closed
Class Parameter__extra_items__
Key with the closed
Class Parameterextra_items
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.
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.
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
.
The new features introduced in this PEP would address several long-standing feature requests in the type system. Previous discussions include:
@final
decorator, the underlying feature request would be addressed by this PEP.TypedDict
can contain arbitrary extra keys (2020).Unpack
mechanism introduced by PEP 692 (2023).TypedDict
(2024).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:
extra_items
can be treated as a pseudo-item.extra_items
.extra_items
and closed
are opt-in only features.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.
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) # OKInteraction 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.
Inheritanceextra_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.
- Changing a field type of a parent TypedDict class in a subclass is not allowed.
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:
extra_items
is read-only
T
extra_items
is not read-only
T
extra_items
is not overridden, the subclass inherits it as-is.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
.
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.
extra_items
is read-only:
T
.S
.extra_items
is not read-only:
T
.S
.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
assignableto a TypedDict type
A
if
B
is
structurallyassignable to
A
. This is true if and only if all of the following are satisfied:
- [If no key with the same name can be found in ``B``, the ‘extra_items’ argument is considered the value type of the corresponding key.]
- For each item in
A
,B
has the corresponding key, unless the item inA
is read-only, not required, and of top value type (ReadOnly[NotRequired[object]]
).- For each item in
A
, ifB
has the corresponding key, the corresponding value type inB
is assignable to the value type inA
.- For each non-read-only item in
A
, its value type is assignable to the corresponding value type inB
, and the corresponding key is not read-only inB
.- For each required key in
A
, the corresponding key is required inB
.- For each non-required key in
A
, if the item is not read-only inA
, the corresponding key is not required inB
.
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:
- For each non-required key in
A
, if the item is not read-only inA
, the corresponding key is not required inB
.
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 # OKInteraction 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 allowedSupported 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.
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.
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:
VT
.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
.)
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:
TypedDict
: a dict
with a fixed set of keys and value types.NotRequired
, Required
, and total=False
: keys that may be missing.ReadOnly
: keys that cannot be modified.TypedDict
type may contain additional keys at runtime that are not specified in the type.closed=True
: disallowing additional keys and restricting inheritance.extra_items=VT
: allowing additional keys with a specified value type.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.
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:
Support a New Syntax of Specifying KeysThe @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.
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:
_: bool
makes the TypedDict special, relative to adding a class argument like extra_items=bool
._: bool
key. While such users have a way to get around the issue, it’s still a problem for them if they upgrade Python (or typing-extensions).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.
AcknowledgmentsThanks 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.
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