Type Hints Basics
9 examples to get you started with Python type hints - 6 basic and 3 intermediate.
Prerequisites
- Python 3.14.0 with mypy or pyright available (
uv add --dev mypy).
Basic Examples
1. Variable and Function Annotations
Document expected types on names and signatures.
name: str = "Ada"
count: int = 0
def greet(user: str) -> str:
return f"Hello, {user}"- Annotations stored in
__annotations__but not enforced at runtime. - Return
-> Nonedocuments no useful return value. - Gradually add hints to stable modules first.
Related: Built-in & Collection Generics - list[int], dict[str, int]
2. Collection Types
Use built-in generics for containers (3.9+).
users: list[str] = []
index: dict[str, int] = {}
unique: set[int] = set()listwithout params means untyped list to strict checkers.- Prefer concrete container types over abstract ones unless API requires flexibility.
- Tuple length can be fixed:
tuple[int, str].
Related: Built-in & Collection Generics - Sequence, Mapping
3. Optional and None
Nullable values use union with None.
def find_user(user_id: int) -> dict | None:
return None
email: str | None = NoneOptional[str]equivalent tostr | None(older style).- Narrow with
if x is not Nonebefore use - checkers refine types. - Do not use
Optionalwhen None is not allowed.
Related: Optional, Union & the | Operator - narrowing
4. Running mypy
Check types statically in CI.
uv run mypy src/# pyproject.toml snippet
[tool.mypy]
python_version = "3.14"
strict = false
warn_return_any = true- Start permissive; tighten per module with
strict = trueoverrides. - mypy reads
pyproject.toml[tool.mypy]section. - Fix errors at boundaries first - HTTP handlers, DB adapters.
Related: mypy & pyright Setup - full config
5. isinstance Narrowing
Combine runtime checks with static narrowing.
def handle(value: str | int) -> str:
if isinstance(value, str):
return value.upper()
return str(value * 2)isinstancehelps mypy/pyright narrow unions.match/casealso narrows when patterns type-specific.- Runtime validation still needed at untrusted boundaries (use Pydantic 2).
Related: Optional, Union & the | Operator - type guards
6. Typed Def with Any Escape Hatch
Any opts out of checking - use sparingly.
from typing import Any
def legacy_bridge(payload: Any) -> dict[str, Any]:
assert isinstance(payload, dict)
return payloadAnyis contagious - returns infect callers.- Prefer
objectwhen any value allowed but not arbitrary operations. - Refine
Anyat system edges over time.
Intermediate Examples
7. Callable Types
Type higher-order functions.
from collections.abc import Callable
def apply(fn: Callable[[int, int], int], a: int, b: int) -> int:
return fn(a, b)Callable[[ArgTypes], ReturnType]documents function shape.ParamSpecpreserves decorator signatures (advanced).- Prefer
Callableover untyped functions in public APIs.
Related: Overloads & Callable Types - ParamSpec
8. Class Attributes
Annotate instance and class variables.
class Counter:
total: int = 0
def __init__(self) -> None:
self.value: int = 0- Class variable annotated in class body; instance vars often in
__init__. ClassVarmarks attributes not overridden on instances.- Dataclasses generate annotations from field definitions.
Related: dataclasses - typed fields
9. Gradual Typing Strategy
Add types module by module.
# mypackage/service.py (typed)
def compute(x: int) -> int:
return x + 1
# mypackage/legacy.py (untyped for now)
def legacy(): ...- Enable
check_untyped_defsafter baseline coverage. - Use
# type: ignore[code]sparingly with comment why. - Track progress with mypy coverage reports per package.
Related: Gradual Typing Strategy - rollout plan
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.