Python TypedDict: Typed Dictionaries

Published on: August 28, 2026
Reading time: 5 minutes
A person typing on a laptop with a Python programming book visible, capturing technology and learning.

Python dictionaries are flexible, but that flexibility can hide mistakes. An API payload may require id, name, and active, while the code describes everything as dict[str, object]. With that generic type, a checker cannot know which keys exist, which keys are optional, or which value type belongs to each key. typing.TypedDict solves this by describing the expected structure of a dictionary without changing its runtime behavior.

This guide covers basic TypedDict declarations, required and optional keys, NotRequired and Required, inheritance, nested API payloads, discriminated variants, and the important boundary between static typing and real data validation.

What TypedDict represents

A TypedDict is a type declaration for dictionaries with known keys. At runtime, the value is still an ordinary dict.

from typing import TypedDict

class User(TypedDict):
    id: int
    name: str
    active: bool

user: User = {
    "id": 10,
    "name": "Ana",
    "active": True,
}

Mypy or Pyright can report a missing key, a misspelled name, or an incompatible value.

wrong_user: User = {
    "id": "10",       # int expected
    "name": "Ana",
    "active": True,
}

Python itself does not reject that dictionary during execution. The benefit appears when the project runs a static checker.

Why dict[str, object] is not enough

dict[str, object] only says that keys are strings and values may be arbitrary objects. It does not connect a particular key with a useful value type.

def display(user: dict[str, object]) -> str:
    return user["name"].upper()  # object does not guarantee upper()

With TypedDict, the checker knows that user["name"] is a string.

def display(user: User) -> str:
    return user["name"].upper()

This precision builds on the concepts in the guide to Python type hints.

Optional keys with total=False

All keys are required by default. Use total=False when every declared key may be absent.

class UserUpdate(TypedDict, total=False):
    name: str
    active: bool
    email: str

This shape is useful for PATCH operations where the caller sends only fields that should change.

def update_user(user_id: int, data: UserUpdate) -> None:
    if "name" in data:
        print(data["name"])

An optional key is not the same as a nullable value. The key may be missing; when present, its value must still match the declared type.

NotRequired and Required

Use NotRequired when only selected fields may be absent. Use Required inside a total=False declaration when one key must remain present.

from typing import NotRequired, Required, TypedDict

class Profile(TypedDict):
    id: int
    name: str
    nickname: NotRequired[str]
    photo: NotRequired[str]

class PartialEvent(TypedDict, total=False):
    kind: Required[str]
    payload: object
    source: str

These markers keep the contract readable without creating several nearly identical types.

Inheritance and schema composition

A TypedDict can inherit fields from another TypedDict.

class Entity(TypedDict):
    id: int

class Product(Entity):
    name: str
    price: float
    stock: int

Inheritance is useful for small shared groups, but complex schema hierarchies become difficult to evolve. Separate input, update, output, and persistence shapes when their requirements differ.

Functional syntax

The functional form is useful when keys are not valid Python identifiers.

Headers = TypedDict(
    "Headers",
    {
        "content-type": str,
        "x-request-id": str,
    },
)

For ordinary identifiers, class syntax is usually clearer and easier to document.

Modeling API responses

TypedDict works well for internal payloads or external responses after validation.

class Address(TypedDict):
    city: str
    state: str
    postal_code: str

class ApiCustomer(TypedDict):
    id: int
    name: str
    address: Address
    tags: list[str]

def customer_city(customer: ApiCustomer) -> str:
    return customer["address"]["city"]

Do not annotate response.json() as a trusted TypedDict without checking the data. Network responses can omit keys, change formats, or contain unexpected values.

TypedDict does not validate at runtime

A TypedDict declaration does not convert values, reject extra keys, or produce validation errors. JSON, form data, queues, and database records still require runtime validation.

import json

text = '{"id": "ten", "name": 99, "active": true}'
data = json.loads(text)
# data exists even though it is incompatible with User

A safe boundary validates the object first and only then returns or narrows it to a precise type. Pydantic, dataclasses with explicit parsing, attrs, or custom validation may be appropriate.

Narrowing optional keys

When a field is NotRequired, check for membership before indexing it.

class Result(TypedDict):
    value: int
    warning: NotRequired[str]

def print_result(result: Result) -> None:
    print(result["value"])
    if "warning" in result:
        print(result["warning"])

get() is also available, but its result commonly includes None, which must be handled.

Structural compatibility and extra keys

TypedDict compatibility is structural. A richer dictionary type may be accepted where a smaller shape is expected, but requiredness, mutability, and value types impose important restrictions.

Avoid relying on obscure assignment rules. Keep consumer contracts focused and run the same checker in local development and CI.

TypedDict as input and output

class NewOrder(TypedDict):
    customer_id: int
    item_ids: list[int]
    coupon: NotRequired[str]

class CreatedOrder(TypedDict):
    id: int
    status: str
    total: float

def create_order(data: NewOrder) -> CreatedOrder:
    return {
        "id": 501,
        "status": "created",
        "total": 149.90,
    }

The signature documents the function boundary and improves autocomplete, reviews, and refactoring.

Discriminated variants with Literal

TypedDict combines especially well with Literal.

from typing import Literal

class Success(TypedDict):
    kind: Literal["success"]
    value: int

class Failure(TypedDict):
    kind: Literal["failure"]
    error: str

Response = Success | Failure

def process(response: Response) -> str:
    if response["kind"] == "success":
        return str(response["value"])
    return response["error"]

The discriminator lets the checker narrow the union to the correct shape.

Schema evolution

When an API evolves, adding optional fields is usually less disruptive than changing required keys. Removing a field or changing its value type should be versioned or migrated carefully. TypedDict helps static analysis reveal affected consumers.

Newer typing ecosystems also provide read-only field concepts. Verify support in the Python version and checker used by the project before adopting newer annotations in a public library.

Common mistakes

  • Using TypedDict as JSON validation: it is static metadata only.
  • Confusing missing with None: key optionality and nullable values are different.
  • Using one schema for every operation: create, patch, and response payloads often need separate types.
  • Casting unvalidated data: a cast silences the checker but does not repair values.
  • Declaring every value as object: this loses most of the precision.
  • Ignoring requiredness and mutability: structural compatibility still has rules.

TypedDict, dataclass, or Pydantic?

Choose TypedDict when the runtime value should remain a dictionary and the primary need is static checking. Choose dataclass when objects should expose attributes, methods, and explicit construction. Choose Pydantic when external data must be validated, converted, documented, or integrated with an API framework.

They can coexist. An input layer may validate with Pydantic and then pass lightweight TypedDict values to code that integrates with dictionary-based libraries.

Conclusion

typing.TypedDict turns informal dictionaries into checkable contracts. It describes required keys, optional keys, nested structures, and discriminated variants without changing the runtime object.

The official Python TypedDict documentation covers totality, inheritance, and requiredness markers. Use it for known dictionary shapes, run a static checker regularly, and keep runtime validation at every untrusted boundary.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    A developer typing code on a laptop with a Python book beside in an office.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python Protocol: Structural Typing

    Learn Python Protocol for structural typing, generic contracts, callbacks, runtime_checkable, testing, dependency injection, and low coupling.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026
    A developer typing code on a laptop with a Python book beside in an office.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python memoryview: Zero-Copy Buffers

    Learn Python memoryview for zero-copy buffers, slices, writable bytearray data, cast, mmap, struct, sockets, and safe lifecycle management.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026
    A person typing on a laptop with a Python programming book visible, capturing technology and learning.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python tracemalloc: Track Memory

    Learn Python tracemalloc to measure peaks, create and compare snapshots, filter allocations, and diagnose memory growth and leaks.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Commuters line up at a subway station platform, showcasing public transportation dynamics.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python queue: Communicate Between Threads

    Learn Python queue for thread communication with FIFO, LIFO, priority, backpressure, task tracking, sentinels, and safe shutdown.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    A vertical macro shot showcasing metallic socket wrenches in a shallow focus arrangement.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python selectors: Many Sockets

    Learn Python selectors to multiplex sockets, handle partial reads and writes, manage buffers, deadlines, wakeups, and backpressure.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Vivid close-up of a green tree python coiled on a branch in the rainforest.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python contextvars: Async Context

    Learn Python contextvars for task-local context, request IDs, logging, copy_context, thread propagation, and safe token restoration.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026