Python type hints transform runtime surprises into editor-time catches. A function that returns Optional[User] forces callers to handle the None case before their code even runs. Combined with mypy in strict mode, type hints give Python the safety guarantees of statically typed languages without losing Python’s flexibility.
⚡ TL;DR: Annotate all function parameters and return types. Use Optional[T] for nullable, Union[A,B] for multiple types, List/Dict/Tuple from typing or built-in generics (Python 3.9+). Protocol for structural typing. TypedDict for typed dicts. Run mypy –strict in CI.
Basic annotations
from typing import Optional
# Python 3.9+ built-in generics (preferred)
def get_user(user_id: int) -> dict[str, str] | None:
pass
# Python 3.8 compatible
from typing import Dict, List, Optional, Tuple
def process(items: List[str], config: Dict[str, int]) -> Optional[str]:
pass
# Function types
from typing import Callable
def apply(fn: Callable[[int, int], int], a: int, b: int) -> int:
return fn(a, b)
Optional, Union, and Literal
from typing import Optional, Union, Literal
# Optional[T] == T | None
def find_user(email: str) -> Optional[dict]: # Can return None
pass
# Union: multiple types
def parse(value: Union[str, int, bytes]) -> str:
if isinstance(value, bytes):
return value.decode()
return str(value)
# Literal: specific values only
Status = Literal['pending', 'active', 'inactive']
def set_status(user_id: int, status: Status) -> None:
pass
# set_status(1, 'deleted') # mypy error: not a valid Status
TypedDict and Protocol
from typing import TypedDict, Protocol, runtime_checkable
# TypedDict: typed dictionary structure
class UserRecord(TypedDict):
id: int
name: str
email: str
is_active: bool
def get_user(user_id: int) -> UserRecord: # Returns must match structure
return {'id': 1, 'name': 'Alice', 'email': 'alice@example.com', 'is_active': True}
# Protocol: structural typing (duck typing with types)
@runtime_checkable
class Serializable(Protocol):
def to_dict(self) -> dict: ...
def to_json(self) -> str: ...
# Any class with these methods satisfies Serializable — no inheritance needed
class User:
def to_dict(self): return {'id': self.id}
def to_json(self): return json.dumps(self.to_dict())
def serialize(obj: Serializable) -> str: return obj.to_json()
mypy strict mode setup
# mypy.ini or pyproject.toml
[mypy]
strict = true
# Enables: --disallow-untyped-defs, --disallow-any-generics,
# --no-implicit-optional, --warn-return-any, and more
# Run in CI:
mypy src/ --strict
# Common fixes for strict mode:
# 1. Add return type to every function
# 2. Replace Dict with dict[str, Any]
# 3. Handle Optional returns before using value
# 4. Add TypeVar for generic functions
- ✅ Annotate all function params and return types — no bare dict or list
- ✅ Use Optional[T] (or T | None in 3.10+) for nullable values
- ✅ TypedDict for structured dicts instead of Dict[str, Any]
- ✅ Protocol for duck-typed interfaces without inheritance
- ✅ Run mypy –strict in CI — type errors as build failures
- ❌ Never use Any as a shortcut — defeats the purpose
- ❌ Never annotate only some functions — partial typing is misleading
Type hints connect to Pydantic models — Pydantic uses type hints at runtime for validation. External reference: mypy documentation.
Recommended Reading
→ Designing Data-Intensive Applications — The essential book every senior developer needs.
→ The Pragmatic Programmer — Timeless engineering wisdom for writing better code.
Affiliate links. We earn a small commission at no extra cost to you.
Free Weekly Newsletter
🚀 Don’t Miss the Next Cheat Code
Join 1,000+ senior developers getting expert JS, Python, AWS and system design secrets weekly.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
