Skip to content
← Back to explainers

Python explainer

Control the Extra Keys of a TypedDict

About 4 min to read. Allow extra time to work through examples.

An optional article on closed=True and extra_items, which let a TypedDict forbid extra keys or give them a type, and on what the site's mypy checks today.

This article uses Python. You can make it your practice mode to browse related content across the site.

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

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

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 closed and extra_items to typing.TypedDict in Python 3.15. On Python 3.12 to 3.14, import TypedDict from typing_extensions 4.13.0 or newer instead.
  • A dict[str, str] parameter is stricter than Mapping[str, str], because a dict can delete or replace any key. To pass a TypedDict there, it needs extra_items=str, and each listed key must be a writable NotRequired[str].
  • The TypedDict specification has the full rules for compatibility and inheritance.

step 24 of 26

Interfaces with Protocols and ABCs

Up next Accept Types Like str | None as Arguments with TypeForm