Why Type Hints Matter
Python's dynamic typing is great for rapid prototyping, but as codebases grow it becomes a liability. Type hints (introduced in PEP 484, Python 3.5) give you the best of both worlds: runtime flexibility with compile-time safety via static analysis tools like mypy and pyright.
Type hints are purely optional annotations — they don't affect runtime behavior. But they enable IDEs to provide accurate autocomplete, catch bugs before deployment, and serve as machine-verified documentation.
Basic Syntax
Function signatures and variable annotations are the most common use cases:
from typing import Optional
def greet(name: str, times: int = 1) -> str:
return (f"Hello, {name}!\n") * times
# Variable annotations
user_id: int = 42
maybe_name: Optional[str] = None # Same as str | None in Python 3.10+
# Python 3.10+ union syntax (preferred)
def process(value: int | str | None) -> str:
if value is None:
return "empty"
return str(value)
Collection Types
Python 3.9+ allows using built-in types directly as generics. For older versions, import from typing:
# Python 3.9+ (preferred)
def process_users(ids: list[int]) -> dict[int, str]:
return {uid: f"user_{uid}" for uid in ids}
def get_coordinates() -> tuple[float, float]:
return (40.7128, -74.0060)
# Nested generics
matrix: list[list[int]] = [[1, 2], [3, 4]]
registry: dict[str, list[tuple[int, str]]] = {}
TypeVar — Generic Functions
TypeVar lets you write functions that work on any type while preserving type relationships:
from typing import TypeVar, Sequence
T = TypeVar("T")
def first(items: Sequence[T]) -> T:
return items[0]
# mypy knows: first([1, 2, 3]) returns int, first(["a", "b"]) returns str
# Bounded TypeVar — T must be a number type
Number = TypeVar("Number", int, float)
def double(value: Number) -> Number:
return value * 2
Protocol — Structural Subtyping
Protocol enables structural subtyping (duck typing with type safety) — any class with the right methods satisfies the protocol, without explicit inheritance:
from typing import Protocol, runtime_checkable
@runtime_checkable
class Drawable(Protocol):
def draw(self) -> None: ...
def resize(self, factor: float) -> None: ...
class Circle:
def draw(self) -> None:
print("Drawing circle")
def resize(self, factor: float) -> None:
self.radius *= factor
# Circle satisfies Drawable without inheriting from it
def render(shape: Drawable) -> None:
shape.draw()
render(Circle()) # OK
isinstance(Circle(), Drawable) # True (requires @runtime_checkable)
Generic Classes
Use Generic[T] to create typed container classes:
from typing import Generic, TypeVar, Iterator
T = TypeVar("T")
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 __iter__(self) -> Iterator[T]:
return iter(self._items)
# mypy tracks the type parameter
int_stack: Stack[int] = Stack()
int_stack.push(42)
int_stack.push("hello") # mypy error: incompatible type "str"; expected "int"
TypedDict — Typed Dictionaries
When working with dictionaries that have a fixed schema (common with JSON APIs), TypedDict provides type safety:
from typing import TypedDict, NotRequired
class UserResponse(TypedDict):
id: int
name: str
email: str
role: NotRequired[str] # Optional key
user: UserResponse = {"id": 1, "name": "Alice", "email": "[email protected]"}
# Validate API JSON using DevKits JSON Formatter at /tools/json-formatter.html
print(user["name"]) # mypy knows this is str
Literal and Final
from typing import Literal, Final
Direction = Literal["north", "south", "east", "west"]
def move(direction: Direction, steps: int) -> None:
print(f"Moving {steps} steps {direction}")
move("north", 5) # OK
move("up", 5) # mypy error: Argument "up" not in Literal
MAX_RETRIES: Final = 3
MAX_RETRIES = 5 # mypy error: Cannot assign to final name
Running mypy
pip install mypy
# Check with strict mode
mypy --strict app.py
# mypy.ini configuration
[mypy]
python_version = 3.11
strict = True
ignore_missing_imports = True
Frequently Asked Questions
Do type hints slow down Python?
No — type hints are ignored at runtime by default. Use from __future__ import annotations to defer evaluation entirely for performance-critical code.
Should I use mypy or pyright?
Both are excellent. mypy is the original and most battle-tested. pyright (used by Pylance in VS Code) is faster and stricter. For new projects, pyright in strict mode catches more issues. Use mypy for CI pipelines.
What is Optional[X] vs X | None?
Optional[X] and X | None are identical. The union syntax (Python 3.10+) is cleaner and preferred in modern code.
For testing regex patterns used in type validators, try the DevKits Regex Tester. To format and validate JSON schemas, use the DevKits JSON Formatter.