Type hints make Python code clearer and enable better tooling. Here's what you need to know.
Basic Type Hints
def greet(name: str) -> str:
return f"Hello, {name}"
age: int = 25
prices: list[float] = [19.99, 29.99]
config: dict[str, int] = {"timeout": 30}Optional and Union
from typing import Optional, Union
# Optional = Union with None
def find_user(id: int) -> Optional[User]:
return users.get(id) # Returns User or None
# Union for multiple types
def process(value: Union[str, int]) -> str:
return str(value)
# Python 3.10+ syntax
def process(value: str | int) -> str:
return str(value)Collections
from typing import List, Dict, Set, Tuple, Sequence
# Before Python 3.9, use typing module
names: List[str] = ["alice", "bob"]
scores: Dict[str, int] = {"alice": 100}
# Python 3.9+: use built-in types
names: list[str] = ["alice", "bob"]
scores: dict[str, int] = {"alice": 100}
# Tuple with specific types
point: tuple[float, float] = (1.0, 2.0)
record: tuple[str, int, bool] = ("name", 42, True)
# Variable-length tuple
values: tuple[int, ...] = (1, 2, 3, 4)
# Sequence for read-only access
def process(items: Sequence[str]) -> None:
for item in items:
print(item)Callable
from typing import Callable
# Function that takes (int, int) and returns int
Operation = Callable[[int, int], int]
def apply(op: Operation, a: int, b: int) -> int:
return op(a, b)
apply(lambda x, y: x + y, 1, 2)
# Any callable
def run(func: Callable[..., None]) -> None:
func()TypeVar (Generics)
from typing import TypeVar, List
T = TypeVar("T")
def first(items: List[T]) -> T:
return items[0]
# Constrained TypeVar
Number = TypeVar("Number", int, float)
def add(a: Number, b: Number) -> Number:
return a + b
# Bound TypeVar
from typing import TypeVar
class Animal:
pass
A = TypeVar("A", bound=Animal)
def process_animal(animal: A) -> A:
return animalGeneric Classes
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()
stack: Stack[int] = Stack()
stack.push(1)
stack.push(2)Protocol (Structural Typing)
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
class Circle:
def draw(self) -> None:
print("Drawing circle")
class Square:
def draw(self) -> None:
print("Drawing square")
def render(shape: Drawable) -> None:
shape.draw()
# Works with any class that has draw()
render(Circle())
render(Square())Literal
from typing import Literal
Mode = Literal["r", "w", "a"]
def open_file(path: str, mode: Mode) -> None:
...
open_file("data.txt", "r") # OK
# open_file("data.txt", "x") # Type errorTypedDict
from typing import TypedDict
class UserDict(TypedDict):
name: str
age: int
email: str
def process_user(user: UserDict) -> None:
print(user["name"])
# Optional keys
class ConfigDict(TypedDict, total=False):
debug: bool
verbose: boolFinal
from typing import Final
MAX_SIZE: Final = 100
# MAX_SIZE = 200 # Type error
class Config:
API_URL: Final[str] = "https://api.example.com"ClassVar
from typing import ClassVar
class Counter:
count: ClassVar[int] = 0 # Class variable
def __init__(self) -> None:
Counter.count += 1Self (Python 3.11+)
from typing import Self
class Builder:
def set_name(self, name: str) -> Self:
self.name = name
return self
def set_value(self, value: int) -> Self:
self.value = value
return self
# Enables proper typing for method chaining
builder = Builder().set_name("test").set_value(42)ParamSpec (Python 3.10+)
from typing import ParamSpec, TypeVar, Callable
P = ParamSpec("P")
R = TypeVar("R")
def logged(func: Callable[P, R]) -> Callable[P, R]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@logged
def add(a: int, b: int) -> int:
return a + bType Guards (Python 3.10+)
from typing import TypeGuard
def is_string_list(val: list[object]) -> TypeGuard[list[str]]:
return all(isinstance(x, str) for x in val)
def process(items: list[object]) -> None:
if is_string_list(items):
# items is now list[str]
for item in items:
print(item.upper())Annotated
from typing import Annotated
# Add metadata to types
UserId = Annotated[int, "User ID must be positive"]
Email = Annotated[str, "Must be valid email format"]
def create_user(id: UserId, email: Email) -> None:
...
# Used by validation libraries like Pydantic
from pydantic import Field
Age = Annotated[int, Field(ge=0, le=150)]Type Aliases
from typing import TypeAlias
# Simple alias
Vector: TypeAlias = list[float]
# Complex alias
JsonValue: TypeAlias = (
str | int | float | bool | None |
list["JsonValue"] | dict[str, "JsonValue"]
)
def parse_json(data: str) -> JsonValue:
import json
return json.loads(data)NewType
from typing import NewType
UserId = NewType("UserId", int)
OrderId = NewType("OrderId", int)
def get_user(user_id: UserId) -> User:
...
def get_order(order_id: OrderId) -> Order:
...
# Type checker catches this mistake
user_id = UserId(123)
# get_order(user_id) # Type error: expected OrderIdcast
from typing import cast
# Tell the type checker "trust me"
value = get_value() # Returns Any
string_value = cast(str, value)
# No runtime effect - just for type checkersTYPE_CHECKING
from typing import TYPE_CHECKING
if TYPE_CHECKING:
# Only imported during type checking, not at runtime
from expensive_module import HeavyClass
def process(obj: "HeavyClass") -> None:
# Use string annotation for forward reference
...Common Patterns
from typing import overload
# Multiple signatures
@overload
def process(x: int) -> int: ...
@overload
def process(x: str) -> str: ...
@overload
def process(x: list[int]) -> list[int]: ...
def process(x):
if isinstance(x, int):
return x * 2
elif isinstance(x, str):
return x.upper()
else:
return [i * 2 for i in x]Best Practices
# Use | instead of Union (3.10+)
def f(x: int | str) -> None: ...
# Use built-in generics (3.9+)
def f(items: list[int]) -> dict[str, int]: ...
# Return None explicitly
def log(msg: str) -> None:
print(msg)
# Use Self for fluent interfaces (3.11+)
# Use ParamSpec for decorators (3.10+)
# Use TypeGuard for type narrowing (3.10+)Type hints are documentation that tools can verify. Start with function signatures, then expand to variables where it helps clarity.
React to this post: