TypedDict
TypedDict lets you describe the exact shape of a dictionary — which keys exist and what type each value should have — so static checkers can validate dict-based data structures like JSON payloads.
Plain dict[str, Any] type hints tell a checker nothing about which keys are actually present or what each value's type should be. TypedDict (from typing, PEP 589) fixes this by letting you declare a dictionary shape as a set of named, typed keys — commonly used for JSON-like data, configuration dicts, and API payloads where a full class would be overkill but structure still matters.
What Is a TypedDict
A TypedDict declares that a dictionary has a specific, fixed set of string keys, each with its own value type. Unlike a regular class, a TypedDict instance IS a plain dict at runtime — TypedDict exists purely for static type checking; it adds no runtime validation, no methods, and no wrapper object.
from typing import TypedDict
class Movie(TypedDict):
title: str
year: int
rating: float
m: Movie = {"title": "Arrival", "year": 2016, "rating": 8.0}
print(type(m)) # <class 'dict'> — it is a real, ordinary dictWhy TypedDict Matters
Dictionaries are extremely common as ad-hoc records — parsed JSON, configuration blocks, database rows fetched as dicts — but a bare dict[str, Any] hint gives a type checker no way to catch a typo'd key or a wrong-typed value. Defining a small class (or NamedTuple) is often not an option when the data genuinely needs dict semantics (e.g. it came from json.loads() and downstream code indexes it with ["key"]). TypedDict bridges this gap: dict-shaped data with a static contract.
Basic Syntax
TypedDict supports two equivalent syntaxes: the class-based form (preferred for readability) and a functional form (useful when a key name is not a valid Python identifier, e.g. contains a hyphen).
from typing import TypedDict
# class-based form
class Point(TypedDict):
x: int
y: int
# functional form — needed for non-identifier keys
Point2 = TypedDict("Point2", {"x": int, "y": int})
Response = TypedDict("Response", {"content-type": str, "status": int})Required and Optional Keys
By default, every key declared in a TypedDict is required. Mark individual keys optional with NotRequired[...] (Python 3.11+), or flip the class-wide default with total=False and mark the required exceptions with Required[...].
total=True is the implicit default: every key must be present. total=False flips that default for the whole class; Required/NotRequired override the default per key and can be mixed with total.
from typing import TypedDict, NotRequired, Required
class UserProfile(TypedDict):
username: str # required
email: str # required
bio: NotRequired[str] # optional — may be absent
class Config(TypedDict, total=False):
debug: bool # optional (total=False makes all optional)
host: Required[str] # required exception, overriding total=False
profile: UserProfile = {"username": "ada", "email": "ada@example.com"} # OK, bio omitted
cfg: Config = {"host": "localhost"} # OK, debug omittedStatic Checking vs Runtime Behavior
This is the single most important thing to understand about TypedDict: it is a purely static construct. Creating a dict that violates the declared shape does NOT raise any error at runtime — only a static type checker (mypy, pyright) flags it.
TypedDict performs NO runtime validation. If you need runtime enforcement of a dict's shape (e.g. validating untrusted JSON from a network request), reach for a third-party library like pydantic, or write explicit validation code — TypedDict alone will not protect you at runtime.
from typing import TypedDict
class Movie(TypedDict):
title: str
year: int
# A type checker flags this as an error (missing "year", wrong type for "title")...
bad: Movie = {"title": 42}
# ...but Python itself runs this line without any exception at all.
print(bad) # {'title': 42}Inheritance and Composition
A TypedDict can inherit from other TypedDicts to build up shared shapes, similar to composing interfaces.
from typing import TypedDict
class HasId(TypedDict):
id: int
class HasTimestamps(TypedDict):
created_at: str
updated_at: str
class Article(HasId, HasTimestamps):
title: str
body: str
a: Article = {
"id": 1,
"created_at": "2026-01-01",
"updated_at": "2026-01-02",
"title": "Hello",
"body": "World",
}Accessing Values
Because a TypedDict value is a real dict, all normal dict operations work — indexing, .get(), iteration, .items(). Static checkers use the declared key types to narrow the value type for d["key"] accesses, but this narrowing disappears if you use dynamic key lookups.
def show(movie: Movie) -> None:
print(movie["title"]) # checker knows this is str
print(movie.get("year", 0)) # checker knows this is int (or the default)Practical Usage
TypedDict is a natural fit for typing JSON API responses, configuration dictionaries loaded from YAML/JSON, and any function boundary where dict-shaped data crosses in or out — while keeping the lightweight dict runtime representation that JSON tooling expects.
from typing import TypedDict
import json
class ApiResponse(TypedDict):
status: int
message: str
def parse_response(raw: str) -> ApiResponse:
data = json.loads(raw)
return data # type checker trusts the declared return shape
resp = parse_response('{"status": 200, "message": "OK"}')
print(resp["status"])Edge Cases and Rules
TypedDict classes cannot be instantiated with isinstance() checks in the normal sense — there is no runtime class distinguishing a Movie dict from any other dict, since both are just dict instances. TypedDict also does not support arbitrary extra keys by default; a checker will flag keys not in the declared shape (this can be relaxed with closed=False/extra_items in newer typing extensions, but treat that as an advanced, evolving feature).
A TypedDict cannot have methods (it inherits from dict, not a normal class body with executable logic) — it is purely a shape declaration.
TypedDict vs dataclass vs NamedTuple
Choose based on what runtime object you actually need.
TypedDict dataclass NamedTuple
Runtime type plain dict real class tuple subclass
Mutability mutable (dict) mutable by default immutable
Methods allowed no yes yes
Best for JSON/dict data rich objects/logic lightweight recordsKey Takeaways
TypedDict declares the shape (keys and value types) of a dictionary for static type checking only.
At runtime, a TypedDict-typed value is an ordinary dict — no validation, no wrapper, no enforcement happens automatically.
All keys are required by default; use NotRequired[...]/Required[...] or class-level total=False to control which keys are optional.
TypedDicts can inherit from other TypedDicts to compose shared key sets.
For runtime validation of untrusted data (e.g. JSON from a network request), use explicit checks or a third-party library like pydantic — TypedDict alone will not catch bad data at runtime.
movie: Movie = {"title": "Arrival"} # missing "year" # Runs fine! No exception, no validation happens.
TypedDict provides zero runtime enforcement; only a static type checker catches shape violations — you must actually run one to get any benefit.
class Config(TypedDict): debug: bool if isinstance(cfg, Config): # TypeError-ish misconception: this always compares against dict ...
if isinstance(cfg, dict) and "debug" in cfg: ...
TypedDict subclasses have no distinct runtime class; isinstance() checks against them don't verify the declared shape (and using a TypedDict directly with isinstance is not meaningful) — check dict-ness and key presence explicitly if you need a runtime check.
class Movie(TypedDict): title: str def summary(self) -> str: # SyntaxError-adjacent misuse: not supported return self["title"] class Movie(TypedDict): title: str def summary(movie: Movie) -> str: return movie["title"]
TypedDict cannot carry methods — it's a pure shape declaration. Use a standalone function or switch to a dataclass if you need behavior attached to the data.
What is a TypedDict's runtime type — is it a distinct class from dict?
No. A TypedDict value is a plain ordinary dict at runtime; TypedDict exists only for static type checking and adds no runtime wrapper or distinct class.
If you assign a dict missing a required key to a TypedDict-annotated variable, what happens when you run the program?
Nothing — the program runs without error. Only a static type checker like mypy or pyright flags the missing key; there is no runtime validation.
How do you mark an individual key as optional in a TypedDict, without changing the totality of the whole class?
Wrap its type in NotRequired[...] (Python 3.11+), e.g. bio: NotRequired[str]. The class-wide total=False flips the default for all keys instead.
When would you use the functional TypedDict syntax instead of the class-based syntax?
When a key name is not a valid Python identifier, such as one containing a hyphen (e.g. 'content-type'), which cannot appear as a class attribute name.
Why is TypedDict not appropriate for validating untrusted JSON from an external API at runtime?
Because TypedDict performs no runtime checks at all — malformed or missing data passes through silently. Use explicit validation code or a library like pydantic for actual runtime enforcement.
Can a TypedDict have methods like a regular class?
No. TypedDict is purely a shape declaration for keys and value types; behavior must live in standalone functions or a different construct like a dataclass.
How does TypedDict inheritance work?
A TypedDict class can inherit from one or more other TypedDict classes, combining their declared keys into a single composed shape.
Define a TypedDict Employee with required name: str and id: int, and an optional manager: str key. Create one valid instance without the optional key.
from typing import TypedDict, NotRequired class Employee(TypedDict): name: str id: int manager: NotRequired[str] e: Employee = {"name": "Priya", "id": 101} print(e)
{'name': 'Priya', 'id': 101} — manager is omitted since it's marked NotRequired, and this is valid both for the checker and at runtime.
What does this print, and does it raise any error at runtime?
It prints {'x': 1, 'y': '2'} <class 'dict'> with no runtime error — a static checker would flag y as wrong-typed (str instead of int), but TypedDict performs no runtime validation, so the program runs normally.
Write two TypedDicts, Base (with id: int) and Extended (inheriting Base, adding name: str), then construct one valid Extended instance.
from typing import TypedDict class Base(TypedDict): id: int class Extended(Base): name: str item: Extended = {"id": 1, "name": "Widget"} print(item)
{'id': 1, 'name': 'Widget'} — Extended combines its own key with the inherited id key from Base, and both are required.
from typing import TypedDict
class Point(TypedDict):
x: int
y: int
p: Point = {"x": 1, "y": "2"}
print(p, type(p))