Type Hints and Static Analysis with mypy

· 5 min read · Updated March 7, 2026 · intermediate
type-hints mypy static-analysis typing type-checking development-tools

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.