Python Dict Type Hints: A Practical Guide
python dict type hint: Learn how to type hint dictionaries in Python using dict[str, int], TypedDict, and nested types for clearer code and better static analysis.
When you write a function that accepts or returns a dictionary, leaving it untyped means the reader and the type checker have to guess what keys and values are present. A python dict type hint such as dict[str, int] makes that contract explicit. This article covers the syntax, the common patterns, and the edge cases you need for real code.
Basic Dictionary Type Hints
The standard generic syntax for a dictionary type hint is dict[KeyType, ValueType]. For example, a mapping from usernames to user IDs can be declared as dict[str, int]. This tells the type checker that every key is a string and every value is an integer.
def user_id_map() -> dict[str, int]: return {"alice": 1, "bob": 2}
The same syntax works for parameters. When you pass a dictionary into a function, annotating the parameter prevents accidental misuse of the wrong type.
def send_email(recipients: dict[str, str]) -> None: for name, address in recipients.items(): print(f"Sending to {name} at {address}")
The generic form is available from Python 3.9 onward. For older versions, you would use typing.Dict instead, but dict[...] is the modern and recommended approach.
Typing Nested Dictionaries
Real-world data often has nested structures. A dictionary that maps product IDs to another dictionary of attributes needs a type hint that reflects both levels.
def get_products() -> dict[str, dict[str, int]]: return { "sku-1": {"price": 100, "stock": 5}, "sku-2": {"price": 250, "stock": 0}, }
Here the outer dictionary has string keys, and each value is itself a dictionary with string keys and integer values. This is read as "a dictionary whose values are dictionaries of string to int."
When nesting gets deeper, the type expression becomes harder to read. In that case, consider defining a TypedDict for the inner structure, which we cover next.
Using TypedDict for Structured Dictionaries
TypedDict is a special construct in typing that lets you define the exact keys and value types for a dictionary. It gives you the clarity of a class without requiring you to define an actual class.
from typing import TypedDict class Product(TypedDict): price: int stock: int def get_product() -> Product: return {"price": 100, "stock": 5}
The type checker now knows that get_product() returns a dictionary with exactly the keys price and stock, both integers. This is more precise than dict[str, int] because it also validates the key names.
TypedDict is especially useful for API responses, configuration files, or any dictionary with a fixed schema. It also supports optional keys using NotRequired (Python 3.11+) or the older total=False syntax.
class Product(TypedDict, total=False): price: int stock: int discount: float
With total=False, every key is optional. The type checker will not complain if a key is missing.
Handling Optional Values and Union Types
Dictionaries often contain values that may be absent or of different types. Use Optional or Union to express this.
from typing import Optional def get_config() -> dict[str, Optional[int]]: return {"timeout": 30, "retries": None}
Here the values can be either an integer or None. This is equivalent to dict[str, int | None] in Python 3.10 and later.
If a dictionary can hold more than one non-None type, use Union or the pipe syntax.
def get_settings() -> dict[str, int | str]: return {"mode": "fast", "level": 3}
The type checker will allow both strings and integers as values, but you lose the ability to know which key maps to which type. For that level of detail, a TypedDict is a better fit.
Runtime Behavior and Performance Considerations
Type hints are not enforced at runtime. A function annotated with dict[str, int] will happily accept a dictionary with string values if you call it directly. The hint exists for static type checkers, editors, and documentation.
This also means there is no performance cost to adding type hints. They are evaluated at function definition time and stored in the __annotations__ attribute, but they do not affect the execution speed of your code.
The real benefit is earlier detection of bugs. Running a tool like mypy or pyright on your codebase catches mismatched key or value types before the code reaches production. This is especially valuable when dictionaries cross module boundaries or are used in public APIs.
Common Pitfalls and Compatibility Notes
One common mistake is using dict without parameters, which is equivalent to dict[Any, Any] and provides no useful information. Always specify the key and value types.
nAnother issue is the Dict from typing. It still works, but the built-in dict[...] is preferred in modern Python. If you support Python 3.8 or earlier, you need from __future__ import annotations to use the built-in syntax in annotations, or stick with typing.Dict.
TypedDict is available from Python 3.8. If you need to support older versions, you can use the typing_extensions package.
Finally, be aware that type hints are not a runtime validation tool. If you need to enforce the shape of a dictionary at runtime, use a library like pydantic or write explicit validation code. Type hints alone will not raise an error for a malformed dictionary.