This article is optional. It covers a feature that is new in Python 3.15, and most code does not need it yet. You can skip it on a first pass.
An open TypedDict can hold more keys than it lists
A TypedDict is open by default. A variable of that type can hold a value
whose type has more keys:
from typing import TypedDict
class User(TypedDict):
name: str
class Admin(User):
role: str
admin: Admin = {"name": "Mina", "role": "editor"}
user: User = admin
print(user) # {'name': 'Mina', 'role': 'editor'}
mypy accepts user: User = admin, because an Admin has every key that a
User needs. So a variable typed User can hold keys that User does not
list, with values of any type. mypy still checks a literal strictly. Writing
{"name": "Mina", "role": "editor"} straight into a User variable is an
error.
Most of the time that is what you want. Sometimes you need to know there are no other keys, or what type their values have.
Forbid extra keys with closed=True
closed=True says the dictionary has no keys beyond the ones it lists:
from typing import TypedDict
class Config(TypedDict, closed=True):
host: str
port: int
class Server(TypedDict):
host: str
port: int
debug: bool
server: Server = {"host": "localhost", "port": 8000, "debug": True}
config: Config = server # mypy error: a Server can have keys Config forbids
mypy also rejects a subclass of Config that adds a key, because a
Config must never hold one.
Give extra keys a type with extra_items
Sometimes extra keys are fine, and you know their type. Tags on a cloud
resource are an example. Every resource has an env and a service tag,
and any other tag is a string too:
from collections.abc import Mapping
from typing import TypedDict
class Tags(TypedDict, extra_items=str):
env: str
service: str
def render(fields: Mapping[str, str]) -> str:
return ", ".join(f"{key}={value}" for key, value in sorted(fields.items()))
tags: Tags = {"env": "prod", "service": "api", "region": "eu"}
print(render(tags)) # env=prod, region=eu, service=api
extra_items=str allows any other key whose value is a str. Every value
in a Tags is then a string, so you can pass it to a Mapping[str, str]
parameter. You cannot pass an open TypedDict there, because a key it does
not list might hold an int. closed=True means the same as
extra_items=Never. Use one keyword or the other, not both.
The site runs mypy 2.4.0. It checks closed=True as this page describes,
but it does not support extra_items yet, so it rejects the Tags
example with Unexpected keyword argument "extra_items".
Check incoming data yourself
closed=True is a rule for the type checker. At runtime a Config is a
plain dict, and nothing stops a JSON payload with extra keys from
reaching your code. Check the keys yourself where the data enters your
program:
def parse_config(data: dict[str, object]) -> Config:
unknown = data.keys() - {"host", "port"}
if unknown:
raise ValueError(f"unknown keys: {sorted(unknown)}")
host, port = data["host"], data["port"]
if not isinstance(host, str) or not isinstance(port, int):
raise ValueError("host must be a str and port an int")
return {"host": host, "port": port}
Check your understanding
Why can you pass a Tags value to a Mapping[str, str] parameter, but not an open TypedDict whose listed values are all strings?
Show answer
Hide answer
An open TypedDict can hold keys it does not list, such as the role of
an Admin stored in a User variable. Those values can have any type, so
mypy cannot know that every value is a string. extra_items=str says that
every extra value is a string, and the listed keys are strings already, so
every value fits Mapping[str, str].
Check your understanding
A vendor's webhook adds new fields from time to time. Should its TypedDict use closed=True?
Show answer
Hide answer
No. closed=True says that there are no other keys, and the vendor
does not promise that. Your types would be wrong the day a new field
arrives. Keep the type open, list the fields you use, and check them where
the payload enters your program. Use extra_items only if the vendor
documents the type of every extra value.
Going further
- PEP 728 added
closedandextra_itemstotyping.TypedDictin Python 3.15. On Python 3.12 to 3.14, importTypedDictfrom typing_extensions 4.13.0 or newer instead. - A
dict[str, str]parameter is stricter thanMapping[str, str], because adictcan delete or replace any key. To pass aTypedDictthere, it needsextra_items=str, and each listed key must be a writableNotRequired[str]. - The TypedDict specification has the full rules for compatibility and inheritance.