The Complete Guide to Python Type Hints in 2026: From Basic to Protocol Classes
Python type hints matured significantly by 2026, with type checkers like Pyright and mypy now standard in production codebases. The combination of type hints plus a type checker catches bugs before runtime.
Here's the practical guide.
Basic Type Hints
def greet(name: str, age: int) -> str:
return f"Hello {name}, you are {age}"
# Type aliases
UserId = int | str # Union type
Vector = list[float]
def scale(vector: Vector, factor: float) -> Vector:
return [x * factor for x in vector]
Optional and None
from typing import Optional
def find_user(user_id: int) -> Optional[str]:
user = db.get(user_id)
return user.name if user else None
# Modern syntax (Python 3.10+)
def find_user(user_id: int) -> str | None:
...
Collections
from typing import TypedDict, NamedTuple
# List of strings
def get_names() -> list[str]:
return ["Alice", "Bob"]
# Dict with specific types
def get_user_counts() -> dict[str, int]:
return {"alice": 10, "bob": 5}
# Set of integers
def get_ids() -> set[int]:
return {1, 2, 3}
# Tuple with specific types
def get_coordinates() -> tuple[float, float]:
return (40.7128, -74.0060)
TypedDict (for dict shapes)
from typing import TypedDict
class User(TypedDict):
id: int
name: str
email: str
active: bool
def create_user(data: User) -> User:
return data
# Type checker catches missing fields
user = create_user({"id": 1, "name": "Alice", "email": "a@b.com", "active": True})
NamedTuple
from typing import NamedTuple
class Point(NamedTuple):
x: float
y: float
def distance(self, other: "Point") -> float:
return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5
p1 = Point(0.0, 0.0)
p2 = Point(3.0, 4.0)
print(p1.distance(p2)) # 5.0
Protocol (structural subtyping)
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
class Circle:
def __init__(self, radius: float):
self.radius = radius
def draw(self) -> None:
print(f"Drawing circle with radius {self.radius}")
class Square:
def __init__(self, side: float):
self.side = side
def draw(self) -> None:
print(f"Drawing square with side {self.side}")
def render(shape: Drawable) -> None:
shape.draw() # Works with any Drawable
render(Circle(1.0)) # OK
render(Square(1.0)) # OK
Generics
from typing import TypeVar, Generic
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 is_empty(self) -> bool:
return len(self._items) == 0
# Usage
stack: Stack[int] = Stack()
stack.push(1)
stack.push(2)
print(stack.pop()) # 2
Callable Types
from typing import Callable
def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
return func(a, b)
result = apply(lambda x, y: x + y, 5, 3) # 8
result = apply(lambda x, y: x * y, 5, 3) # 15
# No-return callable
def log_and_execute(func: Callable[..., None], *args: object) -> None:
print(f"Executing {func.__name__}")
func(*args)
Type Checking Setup
# Install pyright
pip install pyright
# Create config
# pyrightconfig.json
{
"include": ["src"],
"pythonVersion": "3.12",
"typeCheckingMode": "strict"
}
# Run type checker
pyright src/
This article contains affiliate links. If you sign up through the links above, I may earn a commission at no additional cost to you.
Ready to Build Your Online Business?
Get started with Systeme.io for free — All-in-one platform for building your online business with AI tools.
Top comments (0)