Type Hinting in Python: Mypy and Beyond
python dev typing
Python is dynamically typed, but that doesn’t mean we can’t have type safety. Type hints, combined with static analysis tools like mypy, catch bugs before runtime.
Why Type Hints?
# Without types - what does this return?
def process_user(data):
return data.get('name')
# With types - clear contract
def process_user(data: dict[str, Any]) -> str | None:
return data.get('name')
Benefits:
- Documentation: Self-documenting function signatures
- IDE support: Better autocomplete, refactoring
- Bug detection: Catch type errors before production
- Confidence: Safer refactoring, clearer code review
Basic Type Hints
# Primitive types
name: str = "Alice"
age: int = 30
price: float = 19.99
is_active: bool = True
# Function parameters and returns
def greet(name: str) -> str:
return f"Hello, {name}"
# Optional (None allowed)
from typing import Optional
def find_user(id: int) -> Optional[User]:
return User.objects.filter(id=id).first()
# Python 3.10+ syntax
def find_user(id: int) -> User | None:
...
Collection Types
from typing import List, Dict, Set, Tuple
# Lists
names: list[str] = ["Alice", "Bob"]
# Dicts
ages: dict[str, int] = {"Alice": 30, "Bob": 25}
# Sets
unique_ids: set[int] = {1, 2, 3}
# Tuples (fixed length)
point: tuple[int, int] = (10, 20)
record: tuple[str, int, bool] = ("Alice", 30, True)
# Variable-length tuple
scores: tuple[int, ...] = (95, 87, 92, 88)
Type Aliases
from typing import TypeAlias
# Simple alias
UserId: TypeAlias = int
Username: TypeAlias = str
# Complex alias
UserData: TypeAlias = dict[str, str | int | None]
Callback: TypeAlias = Callable[[int, str], bool]
def create_user(data: UserData) -> UserId:
...
Generics
from typing import TypeVar, Generic
T = TypeVar('T')
class Repository(Generic[T]):
def get(self, id: int) -> T | None:
...
def save(self, item: T) -> None:
...
# Usage
user_repo: Repository[User] = Repository()
user: User | None = user_repo.get(1)
Callable Types
from typing import Callable
# Function that takes (int, str) and returns bool
def apply_filter(
items: list[str],
predicate: Callable[[str], bool]
) -> list[str]:
return [item for item in items if predicate(item)]
# Usage
result = apply_filter(names, lambda x: len(x) > 3)
Using Mypy
pip install mypy
mypy your_code.py
Configuration (mypy.ini or pyproject.toml)
# mypy.ini
[mypy]
python_version = 3.11
strict = true
warn_return_any = true
warn_unused_configs = true
[mypy-django.*]
ignore_missing_imports = true
[mypy-celery.*]
ignore_missing_imports = true
Gradual Typing
Start with strict mode off, enable incrementally:
[mypy]
strict = false
[mypy-myapp.critical_module]
disallow_untyped_defs = true
check_untyped_defs = true
Common Patterns
TypedDict for Structured Dicts
from typing import TypedDict
class UserDict(TypedDict):
name: str
email: str
age: int
active: bool
def create_user(data: UserDict) -> User:
# Mypy knows data has name, email, age, active
return User.objects.create(**data)
Literal Types
from typing import Literal
def set_status(status: Literal["pending", "active", "completed"]) -> None:
...
set_status("pending") # OK
set_status("invalid") # Mypy error!
Protocol (Structural Typing)
from typing import Protocol
class Renderable(Protocol):
def render(self) -> str:
...
def display(item: Renderable) -> None:
print(item.render())
# Works with any class that has render() method
class Button:
def render(self) -> str:
return "<button>Click</button>"
display(Button()) # OK - Button has render()
Overloaded Functions
from typing import overload
@overload
def get_items(id: int) -> Item: ...
@overload
def get_items(ids: list[int]) -> list[Item]: ...
def get_items(id_or_ids: int | list[int]) -> Item | list[Item]:
if isinstance(id_or_ids, int):
return Item.objects.get(id=id_or_ids)
return list(Item.objects.filter(id__in=id_or_ids))
Handling Third-Party Libraries
Install Type Stubs
pip install types-requests types-redis django-stubs
Ignore Missing Imports
[mypy-some_untyped_lib.*]
ignore_missing_imports = true
Inline Type Ignore
import untyped_lib # type: ignore[import]
result = sketchy_function() # type: ignore[no-any-return]
Use sparingly.
IDE Integration
VS Code
// settings.json
{
"python.linting.mypyEnabled": true,
"python.linting.mypyArgs": [
"--config-file=mypy.ini"
]
}
PyCharm
Built-in type checking. Optional mypy plugin for stricter checking.
CI Integration
# GitHub Actions
- name: Type Check
run: |
pip install mypy
mypy --strict src/
Fail builds on type errors. Make types mandatory.
Final Thoughts
Type hints in Python are optional but increasingly valuable. Start with function signatures—they provide the most benefit for the least effort.
Use mypy in CI. Enable strict mode gradually. The upfront investment pays off in fewer runtime errors.
Types are documentation that the machine verifies.