Python Typed Dict: Using TypedDict for Type Safety
python typed dict: Learn how to use Python's TypedDict to define dictionaries with fixed keys and types, improving type safety and maintainability.
When you need to pass a dictionary with a fixed set of keys and known value types, the python typed dict pattern—implemented as TypedDict in the typing module—gives you compile-time structure without changing runtime behavior. Unlike a plain dict or a custom class, TypedDict lets you declare the exact shape of a dictionary so type checkers can catch missing keys, wrong value types, and accidental key renames before the code runs.
Defining a TypedDict
A TypedDict is defined as a class that inherits from typing.TypedDict. Each attribute annotation declares a required key and its value type. For example, a user record might look like this:
from typing import TypedDict class User(TypedDict): id: int name: str email: str
This definition tells static type checkers that a User dictionary must contain exactly those three keys, with the specified types. You can also use the functional syntax, which is useful when you want to build the type dynamically:
from typing import TypedDict User = TypedDict('User', {'id': int, 'name': str, 'email': str})
Both forms are equivalent. The class-based syntax is more readable and supports inheritance, which we will cover later.
Using TypedDict in Functions
Once you have a TypedDict, you can use it as a type hint for function parameters and return values. This makes the expected dictionary structure explicit at the call site.
def send_welcome_email(user: User) -> None: print(f"Sending email to {user['email']}") def create_user(id: int, name: str) -> User: return {'id': id, 'name': name, 'email': f"{name.lower()}@example.com"}
When you call create_user, the type checker verifies that the returned dictionary matches the User shape. If you later try to access a key that is not defined, like user['phone'], the type checker will report an error. This catches many bugs that would otherwise only surface at runtime.
Runtime Behavior: TypedDict Is Just a Dictionary
A TypedDict does not create a new runtime type. Instances are plain dict objects. The class definition is only used by static type checkers and is ignored at runtime. This means you cannot use isinstance() to check if a dictionary is a User, and there is no runtime validation of keys or values.
user = {'id': 1, 'name': 'Alice', 'email': 'alice@example.com'} print(type(user)) # <class 'dict'>
Because of this, TypedDict is ideal for data that crosses system boundaries, such as JSON payloads, database rows, or configuration files. The structure is known in advance, but the data itself is naturally represented as a dictionary. If you need runtime validation, consider using a library like Pydantic or a dataclass with custom validation.
Type Checking with mypy and Other Tools
TypedDict is fully supported by mypy, pyright, and the type checker built into most IDEs. To get the benefit, you must run a type checker as part of your development workflow. For example, with mypy, you can run:
mypy your_module.py
If you try to assign a value of the wrong type, mypy will flag it. Consider this function:
def update_email(user: User, new_email: str) -> None: user['email'] = new_email
If you accidentally pass an integer as new_email, mypy will raise an error. The same applies when you construct a dictionary that is missing a required key or includes an extra key. This static analysis is the primary value of TypedDict.
Common Mistakes and How to Avoid Them
One common mistake is trying to instantiate a TypedDict like a regular class. Since it is only a type hint, calling User() will raise a TypeError at runtime. Instead, always create a plain dictionary and let the type checker verify it.
Another mistake is forgetting that all keys are required by default. If you have optional fields, you must mark them with total=False:
class User(TypedDict, total=False): id: int name: str email: str phone: str # optional
With total=False, every key becomes optional. You can also mix required and optional keys by using inheritance:
class BaseUser(TypedDict): id: int name: str class FullUser(BaseUser, total=False): email: str phone: str
Here, id and name are required, while email and phone are optional. This pattern is useful when you have a core set of fields that must always exist and additional fields that may be present.
TypedDict vs Dataclasses vs NamedTuple
When you need a structured data container, you have several options. TypedDict is best when you want to work with dictionary-like data, especially JSON. Dataclasses and NamedTuple create actual classes with attributes, which give you runtime access via dot notation and support methods. They also enforce attribute types at construction time, but they are not dictionaries, so they do not serialize to JSON as directly.
| Feature | TypedDict | Dataclass | NamedTuple |
|---|---|---|---|
| Runtime type | dict | custom class | tuple subclass |
| Access style | user['name'] | user.name | user.name |
| Runtime validation | None | None (unless coded) | None |
| JSON serialization | Direct | Needs conversion | Needs conversion |
| Best for | JSON-like data | Domain objects | Lightweight records |
If your data comes from a JSON API and you only need type safety during development, TypedDict is the simplest choice. If you need to attach methods or perform validation, a dataclass is more appropriate. NamedTuple is useful for immutable records with a small number of fields.
Production Considerations and Maintainability
In a large codebase, TypedDict helps keep the shape of data contracts consistent. When an API response changes, you update the TypedDict definition, and the type checker immediately shows every place that uses the old shape. This is far more reliable than searching for dictionary keys manually.
One operational concern is that TypedDict does not protect against runtime errors from malformed data, such as a missing key in a response from an external service. If you are working with untrusted input, you still need runtime validation. You can combine TypedDict with a validation library that reads the type hints, but that is beyond the scope of the type hint itself.
Another maintainability aspect is that TypedDict definitions can be reused across modules. If multiple functions operate on the same dictionary shape, define the TypedDict in a shared module and import it. This avoids duplication and ensures that changes are propagated consistently.
Finally, be aware that TypedDict is not a runtime performance optimization. It adds no overhead because it is erased at runtime. The benefit is purely in development time and code clarity. For performance-sensitive code, the dictionary operations are identical to a plain dict.