Python TypedDict: Typed Dictionaries
Python dictionaries are everywhere—API responses, configuration files, database records, JSON data. But standard dictionaries are black boxes to type checkers. Access `user['name']` and your type...
Key Insights
- TypedDict provides type hints for dictionaries with fixed string keys, enabling static type checkers to catch errors without runtime overhead or changing your dictionary-based code
- Unlike dataclasses or Pydantic models, TypedDict works with plain dictionaries—perfect for APIs, JSON data, and codebases that must maintain dict compatibility
- Use
total=False,Required[], andNotRequired[]to precisely control which keys are mandatory, making TypedDict flexible enough for real-world data with optional fields
Introduction to TypedDict
Python dictionaries are everywhere—API responses, configuration files, database records, JSON data. But standard dictionaries are black boxes to type checkers. Access user["name"] and your type checker has no idea if that key exists or what type the value should be.
TypedDict solves this by letting you define the exact structure of dictionaries with known keys. It’s pure type annotation—no runtime changes, no new classes, just better type safety for your existing dict-based code.
# Without TypedDict: type checker knows nothing
def get_user_display(user: dict) -> str:
return f"{user['name']} ({user['email']})" # No type checking
# With TypedDict: full type safety
from typing import TypedDict
class User(TypedDict):
name: str
email: str
age: int
def get_user_display(user: User) -> str:
return f"{user['name']} ({user['email']})" # Type checked!
When you run mypy or pyright on the TypedDict version, it verifies that all required keys exist and values match the declared types. But at runtime, User instances are just regular dictionaries—no performance cost, no compatibility issues.
Basic TypedDict Syntax
TypedDict offers two definition styles: class-based and functional. The class-based syntax is cleaner and more common.
from typing import TypedDict
# Class-based syntax (preferred)
class ServerConfig(TypedDict):
host: str
port: int
debug: bool
max_connections: int
# Functional syntax (useful for dynamic creation)
ServerConfig = TypedDict('ServerConfig', {
'host': str,
'port': int,
'debug': bool,
'max_connections': int
})
# Usage is identical—just plain dicts
config: ServerConfig = {
'host': 'localhost',
'port': 8000,
'debug': True,
'max_connections': 100
}
print(config['host']) # Type checker knows this is a str
The functional syntax is necessary when key names aren’t valid Python identifiers or when you’re generating TypedDicts programmatically. For 99% of use cases, stick with the class-based syntax.
Here’s a practical example for a user profile:
class UserProfile(TypedDict):
user_id: int
username: str
email: str
created_at: str # ISO format timestamp
is_active: bool
def create_user(username: str, email: str) -> UserProfile:
return {
'user_id': generate_id(),
'username': username,
'email': email,
'created_at': datetime.now().isoformat(),
'is_active': True
}
Required vs Optional Keys
By default, all TypedDict keys are required (total=True). But real-world data often has optional fields. TypedDict provides several ways to handle this.
from typing import TypedDict, NotRequired, Required
# All keys optional
class PartialUser(TypedDict, total=False):
name: str
email: str
phone: str
# Mix required and optional (Python 3.11+)
class APIResponse(TypedDict):
status: str # Required
data: dict # Required
error: NotRequired[str] # Optional
request_id: NotRequired[str] # Optional
# Make specific keys required when total=False
class OptionalConfig(TypedDict, total=False):
host: Required[str] # Must be present
port: Required[int] # Must be present
timeout: int # Optional
retries: int # Optional
This is invaluable for API responses where some fields only appear on errors:
class UserAPIResponse(TypedDict):
success: bool
user_id: NotRequired[int]
username: NotRequired[str]
error_message: NotRequired[str]
error_code: NotRequired[str]
def handle_response(response: UserAPIResponse) -> None:
if response['success']:
# Type checker knows these might not exist
user_id = response.get('user_id')
if user_id:
print(f"Created user {user_id}")
else:
error = response.get('error_message', 'Unknown error')
print(f"Error: {error}")
Inheritance and Composition
TypedDicts support inheritance, letting you build complex types from simpler ones. This is perfect for extending base models or creating variations.
class BaseUser(TypedDict):
user_id: int
username: str
email: str
class AdminUser(BaseUser):
permissions: list[str]
admin_level: int
can_delete_users: bool
class GuestUser(BaseUser):
session_id: str
expires_at: str
# You can also combine multiple TypedDicts
class Timestamped(TypedDict):
created_at: str
updated_at: str
class UserWithTimestamps(BaseUser, Timestamped):
pass # Inherits all fields from both parents
# Usage
admin: AdminUser = {
'user_id': 1,
'username': 'admin',
'email': 'admin@example.com',
'permissions': ['read', 'write', 'delete'],
'admin_level': 3,
'can_delete_users': True
}
This composition model works well for database models where you have common fields across tables:
class DBRecord(TypedDict):
id: int
created_at: str
updated_at: str
class Product(DBRecord):
name: str
price: float
stock: int
class Order(DBRecord):
user_id: int
product_ids: list[int]
total: float
status: str
TypedDict with Type Checkers
The real power of TypedDict emerges when you run static type checkers. Here’s what mypy catches:
class Article(TypedDict):
title: str
author: str
views: int
# Valid usage
article: Article = {
'title': 'Python TypedDict Guide',
'author': 'Jane Doe',
'views': 1500
}
# Error: missing required key 'views'
incomplete: Article = {
'title': 'Incomplete Article',
'author': 'John Smith'
}
# Error: wrong value type for 'views'
wrong_type: Article = {
'title': 'Bad Article',
'author': 'Bob',
'views': '1500' # Should be int, not str
}
# Error: accessing non-existent key
def print_article(article: Article) -> None:
print(article['published_date']) # Key doesn't exist in TypedDict
Type checkers also validate function returns:
def fetch_article(article_id: int) -> Article:
# Error: return type doesn't match Article
return {
'title': 'Some Article',
'author': 'Author Name'
# Missing 'views' key
}
Real-World Use Cases
TypedDict shines when working with external data formats. Here’s a FastAPI example:
from fastapi import FastAPI
from typing import TypedDict
app = FastAPI()
class CreateUserRequest(TypedDict):
username: str
email: str
password: str
class UserResponse(TypedDict):
user_id: int
username: str
email: str
created_at: str
@app.post("/users")
def create_user(request: CreateUserRequest) -> UserResponse:
# Type checker ensures we return correct structure
user_id = save_user_to_db(request)
return {
'user_id': user_id,
'username': request['username'],
'email': request['email'],
'created_at': datetime.now().isoformat()
}
For configuration files, TypedDict provides structure without overhead:
class DatabaseConfig(TypedDict):
host: str
port: int
database: str
username: str
password: str
pool_size: NotRequired[int]
timeout: NotRequired[int]
class AppConfig(TypedDict):
database: DatabaseConfig
debug: bool
secret_key: str
def load_config() -> AppConfig:
with open('config.json') as f:
config = json.load(f)
return config # Type checker validates structure
Limitations and Alternatives
TypedDict isn’t always the right choice. Here’s when to use alternatives:
Use dataclasses when:
- You need methods on your data structures
- You want default values or post-init processing
- You don’t need dict compatibility
from dataclasses import dataclass
@dataclass
class User:
username: str
email: str
is_active: bool = True
def deactivate(self):
self.is_active = False
Use Pydantic when:
- You need runtime validation
- You’re parsing external data (APIs, JSON)
- You want automatic data conversion
from pydantic import BaseModel, EmailStr
class User(BaseModel):
username: str
email: EmailStr # Validates email format at runtime
age: int
class Config:
validate_assignment = True
Use NamedTuple when:
- You need immutable data
- You want tuple unpacking
- Memory efficiency matters
from typing import NamedTuple
class Point(NamedTuple):
x: float
y: float
p = Point(1.0, 2.0)
x, y = p # Tuple unpacking works
Stick with TypedDict when:
- Working with existing dict-based code
- Interfacing with libraries expecting dicts
- You need zero runtime overhead
- Your data naturally maps to JSON/dict structures
TypedDict is the minimalist choice—pure type safety without changing how your code runs. For APIs, configuration files, and JSON data where dicts are the natural representation, TypedDict provides exactly what you need and nothing you don’t.