Type Hints and Static Analysis with mypy
Python is a dynamically typed language. Variables hold any type, and type checking happens at runtime. This flexibility is great for rapid prototyping, but it also means bugs slip through until your code actually runs. Type hints let you add type information to your code, and mypy uses those hints to catch errors before execution.
Installing mypy
You need Python 3.9 or later to run mypy. Install it with pip:
python3 -m pip install mypy
Run mypy on any Python file to type check it:
mypy program.py
Mypy reports errors without running your code, like a linter. You can still execute your program with python even if mypy finds issues. Type hints do not affect runtime behavior.
Adding Type Annotations
Without type annotations, mypy treats functions as dynamically typed and skips checking them. Consider this function:
def greeting(name):
return 'Hello ' + name
# These calls fail at runtime, but mypy reports nothing
greeting(123)
greeting(b"Alice")
Add type hints to make mypy catch these bugs:
def greeting(name: str) -> str:
return 'Hello ' + name
greeting(3) # Error: Argument 1 to "greeting" has incompatible type "int"; expected "str"
greeting(b'Alice') # Error: Argument 1 to "greeting" has incompatible type "bytes"; expected "str"
greeting("World!") # No error
The name: str annotation specifies the parameter type. The -> str specifies the return type. Once annotated, mypy checks both how you call the function and what happens inside it.
Basic Type Annotations
Here are the most common type annotations:
def process_user(name: str, age: int, active: bool) -> str:
return f"{name} is {age} years old"
def calculate_total(prices: list[float]) -> float:
return sum(prices)
def find_item(items: list[str], target: str) -> int:
return items.index(target)
def get_config(key: str) -> str | None:
return key if key else None
Python 3.10+ supports the match statement with type guards. Earlier versions use typing.Union:
# Python 3.10+
def handle_value(value: int | str) -> str:
match value:
case int():
return f"Integer: {value}"
case str():
return f"String: {value}"
# Python 3.9 and earlier
from typing import Union
def handle_value(value: Union[int, str]) -> str:
if isinstance(value, int):
return f"Integer: {value}"
else:
return f"String: {value}"
Generic Types
The list type accepts a type parameter in square brackets:
def greet_all(names: list[str]) -> None:
for name in names:
print(f"Hello {name}!")
greet_all(["Alice", "Bob"]) # Works
greet_all([1, 2, 3]) # Error: List item 1 has incompatible type "int"
Other common generic types include dict[K, V], set[T], and tuple[T, U, ...]. For functions that accept any iterable, use collections.abc:
from collections.abc import Iterable
def sum_numbers(numbers: Iterable[float]) -> float:
total = 0.0
for num in numbers:
total += num
return total
This accepts lists, tuples, generators, or any other iterable of floats.
Type Inference
Once you annotate a function’s parameters and return type, mypy infers types for local variables inside the function:
def filter_high(numbers: list[float], threshold: float) -> list[float]:
result = [] # mypy knows this is list[float]
for num in numbers:
if num > threshold:
result.append(num) # num is float here
return result
Mypy also narrows types inside conditional branches:
def describe(value: int | str) -> str:
if isinstance(value, int):
return f"Integer with value {value}"
else:
return f"String of length {len(value)}"
After the isinstance check, mypy knows value is int in the first branch and str in the second.
Working with Libraries
Mypy understands the standard library out of the box:
from pathlib import Path
def read_template(template_path: Path, name: str) -> str:
content = template_path.read_text()
return content.replace("USERNAME", name)
Mypy knows that Path.read_text() returns str, so it understands the replacement operation is valid.
For third-party libraries without type hints, install stub packages:
python3 -m pip install types-requests types-PyYAML
Stub packages are named types-<package>. They contain type annotations without actual code.
Strict Mode and Configuration
Run mypy with --strict to enable comprehensive checking:
mypy --strict program.py
This flag enables checks like --disallow-untyped-defs (no unannotated functions) and --no-implicit-optional (must annotate optional parameters explicitly).
For large existing codebases, start without strict mode and enable checks incrementally. Create a mypy.ini or pyproject.toml configuration:
[tool.mypy]
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = false
[[tool.mypy.overrides]]
module = "requests.*"
ignore_missing_imports = true
The ignore_missing_imports option tells mypy to skip checking third-party libraries that lack stubs.
Common Error Messages
Mypy errors are usually self-explanatory, but here are common ones:
Incompatible types:
def double(x: int) -> int:
return x * 2
result = double("hello") # Error: Argument 1 to "double" has incompatible type "str"
Missing return statement:
def maybe(n: bool) -> int:
if n:
return 1 # Error: Missing return statement
Unsupported operands:
def foo(x: str) -> str:
return x + 5 # Error: Unsupported operand types for + ("str" and "int")
Unknown type:
import something # Error: Cannot find implementation or library stub for module
# Hint: Install types-xxx or use `ignore-missing-imports`
Next Steps
You have learned the basics of type hints and mypy. Start adding type annotations to your functions and run mypy on your code. Begin with the --strict flag disabled and enable checks as you annotate more code.
The typing module provides advanced types like Callable, TypeVar, and Protocol for more complex scenarios. The mypy documentation covers these in detail.
For more practice, continue with the next tutorial in this series or explore pytest for testing your code.