TypedDict describes the keys and value types a dictionary should contain for static type checkers. At runtime, the object remains an ordinary dict. This is useful for simple boundaries such as an internal JSON-shaped structure without claiming automatic validation.
Mark required and optional keys
from typing import NotRequired, TypedDict
class UserPayload(TypedDict):
id: int
name: str
nickname: NotRequired[str]
def label(user: UserPayload) -> str:
return user.get("nickname", user["name"])
data: UserPayload = {"id": 7, "name": "Ana"}
print(label(data))
The annotation helps a checker detect a missing id, an unexpected value type, or unsafe access to an optional key. It adds no constructor: UserPayload(id=7, name="Ana") happens to use ordinary dict keyword construction, not a generated model. At runtime, isinstance(data, UserPayload) is not supported.
Express required keys accurately
Classes are total by default, so every declared key is required. total=False makes all keys potentially absent, while Required and NotRequired override that choice per key.
from typing import NotRequired, Required, TypedDict
class PatchUser(TypedDict, total=False):
id: Required[int]
name: str
email: str
reason: NotRequired[str]
Here id must exist, while the other keys may be omitted. Absence differs from a key present with None; use str | None only when None is an accepted value. A key that may be missing and may contain null needs both ideas, for example email: NotRequired[str | None].
Per-key markers often make a mixed contract easier to review. total=False remains useful for patch payloads where nearly every field is optional. Check the minimum Python version of the project: Required and NotRequired are in modern typing, and typing_extensions provides compatibility for older interpreters.
Narrow external data before assigning it
A JSON decoder returns broad runtime values. Do not silence a checker with cast(UserPayload, raw) before validation, because cast() returns the same object and performs no check.
from typing import Any, TypeGuard
def is_user_payload(value: Any) -> TypeGuard[UserPayload]:
if not isinstance(value, dict):
return False
if not isinstance(value.get("id"), int):
return False
if not isinstance(value.get("name"), str):
return False
nickname = value.get("nickname")
return nickname is None or isinstance(nickname, str)
def parse_user(raw: object) -> UserPayload:
if not is_user_payload(raw):
raise ValueError("invalid user payload")
return raw
Real validation should also enforce domain limits such as nonempty names, allowed values, maximum lengths, and whether unknown keys are accepted. Be careful with bool: because it subclasses int, isinstance(True, int) is true. Use type(value) is int when booleans must be rejected.
TypeGuard tells a static checker that the successful branch has the narrower type. It does not certify the implementation, so tests should cover missing keys, wrong values, nulls, booleans, and extra data. A dedicated validation library can be justified for large nested schemas, but TypedDict itself should not be presented as that library.
Reuse shapes through inheritance
Inheritance can add keys without changing runtime behavior:
class Entity(TypedDict):
id: int
class User(Entity):
name: str
nickname: NotRequired[str]
Keep hierarchies shallow. They describe key sets rather than object-oriented behavior, and duplicate key declarations with incompatible types are rejected by checkers. For reusable value fragments, nesting one TypedDict inside another may express the JSON shape more directly.
Typed dictionaries also follow structural compatibility: a value can often contain additional keys and still satisfy a function expecting a smaller shape. Exact assignment rules consider requiredness and mutability, so rely on checker output rather than assuming normal class-subtyping rules.
Read, update, and iterate safely
Indexing a required key is appropriate after validation. For a NotRequired key, use membership checking or .get() and handle the fallback. Avoid using .get() for a required key merely to suppress a warning, because it weakens the invariant deeper in the program.
Static checkers generally expect string-literal keys. A loop over arbitrary strings loses the connection between each key and its value type. When dynamic iteration dominates, Mapping[str, object], a uniform dictionary type, or a validation model may be a better contract.
For read-only consumers, accept the narrowest useful abstraction when possible. A function that only needs name can receive a small TypedDict; a function that treats every value uniformly may receive Mapping[str, object]. Avoid mutating a dictionary through a broader view because required keys could be removed or incompatible values inserted.
Decide between TypedDict and alternatives
Choose TypedDict for dictionary-shaped data already used by APIs, configuration, or serialization, especially when changing runtime representation would be disruptive. Choose a dataclass for an application-owned value with methods, defaults, derived fields, or meaningful equality. Choose Protocol to describe behavior and available attributes instead of keys. Use a runtime schema or explicit parser when untrusted input must be accepted or rejected.
At a healthy boundary, code decodes JSON, validates it, returns a precisely annotated structure, and then lets static checking help internal callers. This separates two jobs: runtime evidence at the edge and development-time guarantees within trusted code.
NotRequired marks a key that may be absent. Required does the reverse in a definition created with total=False. Per-key markers usually communicate the contract more clearly than making every key optional.
Do not treat TypedDict as proof that an HTTP response is trustworthy. First verify the value is an object, required keys exist, and types and limits match the contract. Only then pass the validated structure deeper into the application.
If the object needs invariants, methods, or a distinct identity, consider a dataclass. The Python type hints guide explains how these annotations fit a static-analysis workflow.
The official TypedDict documentation, accessed July 22, 2026, covers inheritance, generic forms, and introspection of required and optional keys.
Review the boundary
List required, optional, and nullable keys separately. Validate one complete object, one minimal object, and failures for every required key. Run the static checker with an intentional wrong value to confirm the annotation is included in its configured scope.
Keep transport details near the boundary. If business code repeatedly checks strings, normalizes values, or handles missing keys, convert the validated dictionary into a domain model. Conversely, do not create a class solely to rename a stable JSON structure. The useful choice keeps uncertainty at the edge and gives internal callers the clearest contract.