Introduction to Type Hints
Python is dynamically typed, but since Python 3.5, you can add type hints (also called type annotations) to your code. Type hints do not affect runtime behavior — they are used by tools like mypy, IDEs, and linters to catch bugs before your code runs. They also serve as excellent documentation.
# Basic type hints
name: str = "Alice"
age: int = 30
height: float = 5.7
is_active: bool = True
# Function type hints
def greet(name: str) -> str:
return f"Hello, {name}!"
def add(a: int, b: int) -> int:
return a + b
# None return type
def log_message(message: str) -> None:
print(f"[LOG] {message}")
# Optional parameters
def find_user(user_id: int, active_only: bool = True) -> str:
return f"User {user_id}"
Collection Types
# Python 3.9+ - use built-in types directly
numbers: list[int] = [1, 2, 3]
coordinates: tuple[float, float] = (3.14, 2.72)
unique_ids: set[str] = {"abc", "def"}
scores: dict[str, int] = {"Alice": 95, "Bob": 87}
# Variable-length tuples
values: tuple[int, ...] = (1, 2, 3, 4, 5)
# Nested types
matrix: list[list[int]] = [[1, 2], [3, 4]]
users: dict[str, list[str]] = {
"admins": ["Alice", "Bob"],
"users": ["Charlie", "Diana"],
}
# Python 3.8 and earlier - use typing module
from typing import List, Dict, Tuple, Set
numbers: List[int] = [1, 2, 3] # Legacy syntax
Union, Optional, and Advanced Types
from typing import Optional, Union
# Union types - can be one of several types
# Python 3.10+ syntax
def process(value: int | str) -> str:
return str(value)
# Pre-3.10 syntax
def process_old(value: Union[int, str]) -> str:
return str(value)
# Optional - shorthand for Union[X, None]
def find_user(user_id: int) -> Optional[dict]:
"""Returns user dict or None if not found."""
if user_id == 1:
return {"id": 1, "name": "Alice"}
return None
# Python 3.10+ equivalent
def find_user_new(user_id: int) -> dict | None:
pass
# Literal types - restrict to specific values
from typing import Literal
def set_color(color: Literal["red", "green", "blue"]) -> None:
print(f"Color set to {color}")
set_color("red") # OK
# set_color("purple") # mypy error!
# TypeAlias for complex types
type UserId = int
type UserMap = dict[UserId, str]
# Pre-3.12 syntax:
from typing import TypeAlias
UserId: TypeAlias = int
Callable and Protocol Types
from typing import Callable, Protocol
# Callable - type hint for functions
def apply_operation(
values: list[int],
operation: Callable[[int], int]
) -> list[int]:
return [operation(v) for v in values]
result = apply_operation([1, 2, 3], lambda x: x ** 2)
print(result) # [1, 4, 9]
# Callable with keyword args
Handler = Callable[[str, int], bool]
# Protocol - structural subtyping (duck typing with types)
class Renderable(Protocol):
def render(self) -> str: ...
class HTMLElement:
def render(self) -> str:
return "Hello"
class MarkdownText:
def render(self) -> str:
return "# Hello"
def display(item: Renderable) -> None:
"""Accepts any object with a render() -> str method."""
print(item.render())
display(HTMLElement()) # Works
display(MarkdownText()) # Works - no inheritance needed!
Generics
from typing import TypeVar, Generic
# TypeVar for generic functions
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
x: int = first([1, 2, 3]) # T is inferred as int
y: str = first(["a", "b", "c"]) # T is inferred as str
# Generic classes
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
def peek(self) -> T:
return self._items[-1]
def is_empty(self) -> bool:
return len(self._items) == 0
int_stack: Stack[int] = Stack()
int_stack.push(42)
# int_stack.push("hello") # mypy error!
# Python 3.12+ syntax (simpler!)
def first_new[T](items: list[T]) -> T:
return items[0]
class Stack_new[T]:
def __init__(self) -> None:
self._items: list[T] = []
Running mypy
# Install mypy
# pip install mypy
# Run type checking
# mypy myfile.py
# mypy src/
# Configuration in pyproject.toml
# [tool.mypy]
# python_version = "3.12"
# strict = true
# warn_return_any = true
# warn_unused_configs = true
# disallow_untyped_defs = true
Key Takeaways
- Type hints are optional: They do not affect runtime but improve code quality
- Use modern syntax: list[int] over List[int], int | None over Optional[int]
- Protocol for duck typing: Define interfaces without inheritance
- Run mypy in CI: Catch type errors before they reach production