The typing module in the Python standard library provides the building blocks for type hints: annotations that describe what types of values functions expect and return. Type hints are ignored at runtime. Static type checkers like mypy read them instead, and IDEs use them for autocompletion and error detection.
def greet(name: str, count: int = 1) -> str:
return f"Hello, {name}! " * count
print(greet("Maya", 3)) # Hello, Maya! Hello, Maya! Hello, Maya!name: str says the first argument must be a string. count: int = 1 says the second argument is an integer with a default of 1. -> str says the function returns a string.
These annotations are stored but not enforced by Python itself.
Basic type annotations
Python's built-in types can be used directly as annotations, without importing anything from typing. This covers bool, int, float, str, bytes, and every other built-in scalar type.
def process(active: bool, count: int, ratio: float, label: str) -> bytes:
return f"{label}: {count * ratio}".encode()
result: bytes = process(True, 10, 1.5, "total")
print(type(result)) # <class 'bytes'>You can also annotate variables: result: bytes = .... This is useful for complex expressions where the type is not obvious from the initial value.
Collection types
Use the built-in generic syntax (Python 3.9+) for lists, dicts, tuples, and sets.
def get_scores() -> list[int]:
return [92, 85, 78]
def lookup(ids: list[int]) -> dict[int, str]:
return {1: "Maya", 2: "Devin"}
def coordinates() -> tuple[float, float]:
return 40.7128, -74.0060
def unique(items: list[str]) -> set[str]:
return set(items)| Annotation | Meaning |
|---|---|
| list[int] | List of integers |
| dict[str, int] | Dict with string keys and integer values |
| tuple[int, str, bool] | Tuple with three fixed-type elements |
| tuple[int, ...] | Tuple of any length, all integers |
| set[float] | Set of floats |
For Python 3.8 and earlier, import these from typing: List[int], Dict[str, int], Tuple[int, str].
Optional and Union
When a value can be None or one of several types, use Optional and Union, or the | operator (Python 3.10+).
def find_user(user_id: int) -> str | None:
users = {1: "Maya", 2: "Devin"}
return users.get(user_id)
def parse(value: int | str) -> int:
if isinstance(value, str):
return int(value)
return value
print(find_user(1)) # Maya
print(find_user(99)) # Nonestr | None means the function returns a string or None. int | str means the argument accepts either type. These are equivalent to Optional[str] and Union[int, str] from earlier Python versions.
Callable: annotating function parameters
Use Callable to annotate parameters that expect a function.
from typing import Callable
def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
return func(a, b)
def add(x: int, y: int) -> int:
return x + y
print(apply(add, 3, 5)) # 8Callable[[int, int], int] describes a function that takes two ints and returns an int. The first list contains the argument types; the last element is the return type.
TypedDict: typed dictionaries
TypedDict defines a dictionary with a specific key structure, useful for JSON-like data.
from typing import TypedDict
class User(TypedDict):
name: str
age: int
email: str | None
def greet(user: User) -> str:
return f"Hello, {user['name']}!"
u: User = {"name": "Maya", "age": 28, "email": None}
print(greet(u)) # Hello, Maya!Type checkers validate that the dictionary has the correct keys with the correct types. All keys are required by default. For optional keys, use total=False or mark individual keys with NotRequired.
Naming complex types
Give meaningful names to complex type annotations with the type statement (Python 3.12+).
type Vector = list[float]
type Matrix = list[Vector]
type UserRecord = dict[str, str | int | bool]
def dot_product(a: Vector, b: Vector) -> float:
return sum(x * y for x, y in zip(a, b))Vector is easier to read and change than repeating list[float] everywhere. For Python 3.9 through 3.11, use typing.TypeAlias instead: Vector: TypeAlias = list[float]. The typing.TypeAlias form still works in newer versions but is considered legacy now that the type statement covers the same need directly in the language syntax.
Generics with TypeVar
Use TypeVar to write functions and classes that work with multiple types while preserving type relationships.
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T | None:
return items[0] if items else None
print(first([1, 2, 3])) # 1
print(first(["a", "b"])) # afirst([1, 2, 3]) is inferred as int | None. first(["a", "b"]) is inferred as str | None. The type checker preserves the relationship between the input list type and the return type.
Practical example: typed API client
Use type hints throughout a small module to make the interface clear. Two TypedDicts describe the shapes of the request configuration and the parsed response.
from typing import TypedDict
import json
class ApiConfig(TypedDict):
base_url: str
timeout: int
retries: int | None
class ApiResponse(TypedDict):
status: int
data: dict[str, object] | None
error: str | NoneWith both shapes defined, the functions that build a request URL and parse a raw response can reference them directly in their signatures:
def build_url(config: ApiConfig, path: str) -> str:
return f"{config['base_url']}{path}"
def parse_response(raw: str) -> ApiResponse:
data = json.loads(raw)
return {
"status": data.get("status", 200),
"data": data.get("data"),
"error": data.get("error"),
}Calling build_url with a config dict that matches the ApiConfig shape produces the full request URL, and a type checker would flag it immediately if a required key like base_url were missing:
config: ApiConfig = {"base_url": "https://api.example.com", "timeout": 30, "retries": None}
url = build_url(config, "/users")
print(url)The base_url and path are joined directly, producing the full endpoint URL for this particular request, exactly as build_url's return type annotation promises:
https://api.example.com/usersThe type hints document the shape of configuration and response objects. A type checker catches mismatched keys, wrong types, and missing fields before the code runs.
Common mistakes
Using typing.List instead of list for annotations. Since Python 3.9, prefer list[int] over typing.List[int]. The built-in generic syntax is shorter and does not require an import.
Thinking type hints are enforced at runtime. Python never enforces type hints. They are documentation and tooling aids. Use mypy or pyright (in VS Code) to check them.
Over-annotating obvious types. x: int = 5 is unnecessary because the type is obvious. Use annotations on function signatures, complex variables, and module boundaries where they add clarity.
Forgetting that None is not included by default. def find(id: int) -> str: says the function always returns a string. If it can return None, write -> str | None. This is one of the most common bugs type checkers catch.
Rune AI
Key Insights
- Annotate function parameters and return types with
def greet(name: str) -> str:. - Use
list[int],dict[str, int],tuple[int, str]for built-in generics (Python 3.9+). - Use
Optional[str]orstr | Nonewhen a value can beNone. - Use
Union[int, str]orint | strfor values that accept multiple types. - Use
Callable[[int, int], int]to annotate function parameters. - Use
TypedDictfor dictionaries with a known key structure. - Type hints are optional at runtime; use mypy or pyright to check them.
Frequently Asked Questions
Do type hints affect runtime performance?
What is the difference between Optional[X] and Union[X, None]?
Should I add type hints to all my code?
Conclusion
Type hints make Python code more readable and help catch bugs before runtime. Use the built-in generics like list[int] and dict[str, int] (Python 3.9+) for simple cases, and the typing module for advanced types like Optional, Union, Callable, and TypedDict. Combine hints with a type checker like mypy for the full benefit.
More in this topic
Format Dates and Times in Python
Learn how to use strftime and strptime to format dates as strings and parse date strings into Python datetime objects.
What Is the Python Standard Library?
Learn what the Python standard library is, why it matters, and how to use its built-in modules without installing anything extra.
Encode and Decode Base64 Data in Python
Learn how to use Python's base64 module to encode binary data as text and decode Base64 strings back to bytes.