The typing module provides type hints for static analysis. Here's everything you need for everyday use.

Basic Types

# Built-in types (no import needed in 3.9+)
x: int = 1
y: float = 2.0
z: str = "hello"
flag: bool = True
data: bytes = b"data"
 
# None
def no_return() -> None:
    print("nothing")

Collections

Python 3.9+ uses built-in types directly:

# Modern syntax (3.9+)
numbers: list[int] = [1, 2, 3]
mapping: dict[str, int] = {"a": 1}
items: set[str] = {"x", "y"}
coords: tuple[int, int] = (1, 2)
 
# Pre-3.9 (from typing import)
from typing import List, Dict, Set, Tuple
numbers: List[int] = [1, 2, 3]

Optional and Union

from typing import Optional, Union
 
# Optional = Union[X, None]
def find(key: str) -> Optional[str]:
    return cache.get(key)
 
# Union for multiple types
def process(value: Union[int, str]) -> str:
    return str(value)
 
# Modern syntax (3.10+)
def find(key: str) -> str | None:
    return cache.get(key)
 
def process(value: int | str) -> str:
    return str(value)

Callable

from typing import Callable
 
# Function that takes two ints, returns str
Handler = Callable[[int, int], str]
 
def apply(fn: Callable[[int], int], x: int) -> int:
    return fn(x)
 
# Any callable
from typing import Any
fn: Callable[..., Any]

TypeVar and Generic

from typing import TypeVar, Generic
 
T = TypeVar('T')
 
def first(items: list[T]) -> T:
    return items[0]
 
# Bounded TypeVar
Number = TypeVar('Number', int, float)
 
def double(x: Number) -> Number:
    return x * 2
 
# Generic class
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()

Literal

from typing import Literal
 
Mode = Literal["r", "w", "a"]
 
def open_file(path: str, mode: Mode) -> None:
    pass
 
open_file("test.txt", "r")  # OK
open_file("test.txt", "x")  # Type error

TypedDict

from typing import TypedDict
 
class User(TypedDict):
    name: str
    age: int
    email: str
 
def process_user(user: User) -> None:
    print(user["name"])
 
# Optional keys
class Config(TypedDict, total=False):
    debug: bool
    timeout: int

Protocol (Structural Subtyping)

from typing import Protocol
 
class Drawable(Protocol):
    def draw(self) -> None: ...
 
class Circle:
    def draw(self) -> None:
        print("Drawing circle")
 
def render(shape: Drawable) -> None:
    shape.draw()
 
# Circle matches Drawable without explicit inheritance
render(Circle())

Final and ClassVar

from typing import Final, ClassVar
 
class Config:
    VERSION: Final[str] = "1.0.0"  # Can't reassign
    instance_count: ClassVar[int] = 0  # Class variable
 
MAX_SIZE: Final = 100  # Inferred type

Self Type (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

Type Aliases

from typing import TypeAlias
 
# Simple alias
Vector: TypeAlias = list[float]
 
# Complex alias
JSON: TypeAlias = dict[str, "JSON"] | list["JSON"] | str | int | float | bool | None
 
# NewType for distinct types
from typing import NewType
UserId = NewType('UserId', int)
 
def get_user(user_id: UserId) -> User:
    pass
 
# Must explicitly convert
user_id = UserId(123)

Any and object

from typing import Any
 
# Any: opt out of type checking
def dangerous(x: Any) -> Any:
    return x.whatever()
 
# object: base of all types, but type-safe
def safe(x: object) -> str:
    return str(x)
    # x.whatever()  # Error: object has no attribute 'whatever'

Overload

from typing import overload
 
@overload
def process(x: int) -> int: ...
@overload
def process(x: str) -> str: ...
 
def process(x: int | str) -> int | str:
    if isinstance(x, int):
        return x * 2
    return x.upper()

Type Guards (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]
        print(items[0].upper())

ParamSpec (3.10+)

from typing import ParamSpec, TypeVar, Callable
 
P = ParamSpec('P')
R = TypeVar('R')
 
def decorator(fn: Callable[P, R]) -> Callable[P, R]:
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        print("Before")
        return fn(*args, **kwargs)
    return wrapper

Quick Reference

TypeMeaning
int, str, floatBasic types
list[T]List of T
dict[K, V]Dict with K keys, V values
tuple[T, ...]Variable-length tuple
tuple[T, U]Fixed-length tuple
T | NoneOptional (3.10+)
Optional[T]Optional (pre-3.10)
T | UUnion (3.10+)
Callable[[Args], Ret]Function type
TypeVar('T')Generic type variable
AnyOpt out of checking

Version Guide

FeatureVersion
list[int] syntax3.9+
X | Y union3.10+
TypeGuard3.10+
ParamSpec3.10+
Self3.11+
TypedDict Required/NotRequired3.11+

Type hints are documentation that tools can verify. Use them.

React to this post: