TypedDict: Strongly-Typed Dictionaries Guide
Dictionaries are powerful in Python, but Dict[str, Any] loses type information about individual keys and values. TypedDict lets you define a dictionary with a fixed set of keys, each with its own type. This provides the flexibility of dictionaries with the type safety of named classes, making your code more readable and enabling better IDE support and type checking.
What Is TypedDict and Why Use It?
A TypedDict defines a dictionary type with a known set of keys and their corresponding value types. Unlike regular dictionaries, TypedDict enforces that only specified keys are present and that values have the correct types:
from typing import TypedDict
class User(TypedDict):
"""A user with name, email, and age."""
name: str
email: str
age: int
# Correct: all required keys present with correct types
user: User = {"name": "Alice", "email": "[email protected]", "age": 30}
# Incorrect: missing 'age' key
incomplete: User = {"name": "Bob", "email": "[email protected]"} # mypy: error
# Incorrect: wrong type for 'age'
wrong_type: User = {"name": "Charlie", "email": "[email protected]", "age": "30"} # mypy: error
# Incorrect: extra key not in the TypedDict
extra: User = {"name": "Diana", "email": "[email protected]", "age": 25, "role": "admin"} # mypy: error
mypy catches these errors at type-check time, preventing typos and missing fields from slipping into production.
Defining TypedDict: Class vs. Functional Syntax
There are two ways to define a TypedDict. The class syntax is more readable:
from typing import TypedDict
# Class syntax (preferred)
class Product(TypedDict):
"""A product with name, price, and stock."""
name: str
price: float
in_stock: bool
The functional syntax is useful for programmatically generating TypedDicts or when you have complex constraints:
# Functional syntax
Product = TypedDict("Product", {
"name": str,
"price": float,
"in_stock": bool,
})
Both are equivalent. The class syntax is more Pythonic and easier to read in most cases.
Required and Optional Keys
By default, all keys in a TypedDict are required. Make a key optional with the Required and NotRequired utilities (Python 3.11+) or by moving it to a separate base class:
from typing import TypedDict, NotRequired
class Config(TypedDict):
"""Configuration with optional timeout."""
host: str
port: int
timeout: NotRequired[int] # Optional in Python 3.11+
# Correct: only required keys present
minimal: Config = {"host": "localhost", "port": 8080}
# Also correct: optional key included
with_timeout: Config = {"host": "localhost", "port": 8080, "timeout": 30}
In Python 3.10 and earlier, use inheritance to make some keys optional:
from typing import TypedDict
class ConfigRequired(TypedDict):
host: str
port: int
class ConfigOptional(TypedDict, total=False):
timeout: int
class Config(ConfigRequired, ConfigOptional):
"""Merge required and optional keys."""
pass
# Now both are valid:
minimal: Config = {"host": "localhost", "port": 8080}
with_timeout: Config = {"host": "localhost", "port": 8080, "timeout": 30}
The total=False parameter makes all keys in that TypedDict optional.
Accessing and Modifying TypedDict Values
TypedDicts are dictionaries at runtime, so access works normally. But mypy tracks types:
from typing import TypedDict
class Book(TypedDict):
title: str
author: str
pages: int
book: Book = {"title": "1984", "author": "George Orwell", "pages": 328}
# Access: mypy knows the type of each value
title: str = book["title"]
pages: int = book["pages"]
# Modification: mypy checks types
book["pages"] = 350 # OK: int assigned to int
book["pages"] = "350" # mypy: error: Incompatible types
# Adding new keys: mypy rejects keys not in the TypedDict
book["year"] = 1949 # mypy: error: Extra key "year"
To allow extra keys, use total=False for the extra keys or extend the TypedDict with additional fields.
Nested TypedDicts and Complex Structures
TypedDicts can be nested and combined to model complex data structures:
from typing import TypedDict, List
class Address(TypedDict):
street: str
city: str
zipcode: str
class Person(TypedDict):
name: str
age: int
address: Address
class Company(TypedDict):
name: str
employees: List[Person]
# Build a typed structure
company: Company = {
"name": "Acme Corp",
"employees": [
{
"name": "Alice",
"age": 30,
"address": {"street": "123 Main St", "city": "Boston", "zipcode": "02101"},
},
{
"name": "Bob",
"age": 28,
"address": {"street": "456 Oak Ave", "city": "NYC", "zipcode": "10001"},
},
],
}
# Access nested values with full type safety
first_employee_city: str = company["employees"][0]["address"]["city"]
TypedDict nesting is useful for JSON parsing, API responses, and configuration files.
Real-World Example: API Response Typing
TypedDicts excel at typing API responses and JSON data:
from typing import TypedDict, NotRequired, List
import json
from urllib.request import urlopen
class GitHubUser(TypedDict):
"""GitHub user profile."""
login: str
id: int
avatar_url: str
public_repos: int
followers: int
followers_url: NotRequired[str] # Optional
class GitHubRepo(TypedDict):
"""A GitHub repository."""
name: str
url: str
description: NotRequired[str]
class GitHubResponse(TypedDict):
"""GitHub API response for a user."""
user: GitHubUser
repos: List[GitHubRepo]
def fetch_github_user(username: str) -> GitHubUser:
"""Fetch user data from GitHub API and return typed response."""
url = f"https://api.github.com/users/{username}"
with urlopen(url) as response:
data = json.loads(response.read())
# Type checking ensures the API response matches expectations
user: GitHubUser = {
"login": data["login"],
"id": data["id"],
"avatar_url": data["avatar_url"],
"public_repos": data["public_repos"],
"followers": data["followers"],
}
return user
# mypy knows the exact fields of the returned user
user = fetch_github_user("torvalds")
print(user["login"]) # Type: str
print(user["followers"]) # Type: int
Comparing TypedDict vs. Dataclass vs. NamedTuple
Three ways to create structured data in Python:
| Aspect | TypedDict | Dataclass | NamedTuple |
|---|---|---|---|
| Syntax | Lightweight, dict-like | Class with decorator | Class with inheritance |
| Runtime | Pure dict at runtime | True class instance | Immutable tuple subclass |
| Mutability | Mutable | Mutable by default | Immutable |
| Inheritance | Via composition | Via subclassing | Via subclassing |
| Use case | JSON/API data | Object-oriented code | Immutable records |
| Performance | Fastest (native dict) | Normal class | Compact (tuple) |
Use TypedDict for JSON parsing and API responses. Use dataclasses for general object-oriented code. Use NamedTuple for immutable records.
TypedDict in Function Signatures
TypedDicts shine when used in function signatures to document expected inputs and outputs:
from typing import TypedDict, Callable
class ValidationResult(TypedDict):
"""Result of a validation check."""
valid: bool
errors: list[str]
def validate_email(email: str) -> ValidationResult:
"""Validate an email address and return structured results."""
errors: list[str] = []
if "@" not in email:
errors.append("Missing @ symbol")
if "." not in email.split("@")[-1]:
errors.append("Missing domain extension")
return {
"valid": len(errors) == 0,
"errors": errors,
}
# Call and use: mypy knows the exact keys and types
result = validate_email("alice@example")
if result["valid"]:
print("Email is valid")
else:
for error in result["errors"]: # mypy: error is str
print(f" - {error}")
Key Takeaways
- TypedDict defines a dictionary with fixed keys and value types, providing type safety without class overhead
- Use class syntax:
class MyDict(TypedDict): key: type - All keys are required by default; use
NotRequired[T](Python 3.11+) or inheritance to make keys optional - TypedDicts are pure dictionaries at runtime—they have no overhead
- Nest TypedDicts to model complex structures like JSON and API responses
- TypedDicts are ideal for JSON parsing, configuration, and API response typing
- For general OOP, prefer dataclasses; for immutable records, prefer NamedTuple; for JSON, prefer TypedDict
Frequently Asked Questions
Are TypedDicts the same as regular dicts at runtime?
Yes. TypedDict is a typing construct only. At runtime, a TypedDict is just a regular Python dict with no special behavior or overhead. The type checking happens at compile time via mypy.
Can I use TypedDict with JSON loads()?
Yes, by casting: data: MyDict = json.loads(text) # type: ignore. Or cast more precisely: data: MyDict = cast(MyDict, json.loads(text)) from typing.cast.
What if my API response has unpredictable extra keys?
Use total=False for those keys, or use a regular Dict[str, Any] if the structure is too loose. TypedDict is best for well-defined, predictable structures.
Can I have a list of different TypedDict types?
Yes: list[UserDict | AdminDict] or list[Union[UserDict, AdminDict]]. mypy will require narrowing to access specific keys.
How do I make a field a union of multiple types in TypedDict?
Use Union or the pipe syntax: class Config(TypedDict): value: int | str. In Python 3.10+, int | str is preferred.
Further Reading
- Python
typing.TypedDictDocumentation — Official reference and examples - PEP 589: TypedDict — Full specification for TypedDict
- mypy TypedDict Guide — mypy-specific TypedDict behavior and limitations
- Real Python: TypedDict — Practical examples and best practices