typing

Updated March 13, 2026 · Modules
typing type-hints generics annotations stdlib

The typing module provides runtime support for type hints introduced in PEP 484 and subsequent PEPs. It enables developers to annotate functions, variables, and classes with expected types, which static analysis tools like mypy can use to catch type errors before runtime.

Syntax

from typing import TypeVar, Generic, Protocol, Optional, Union, List, Dict

Key Type Constructs

TypeVar

TypeVar creates a generic type variable that can be any one type within a bound:

from typing import TypeVar

T = TypeVar('T')

def first_element(lst: list[T]) -> T | None:
    return lst[0] if lst else None

print(first_element([1, 2, 3]))
# 1
print(first_element(['a', 'b']))
# a

Generic

Generic is the base class for generic types:

from typing import TypeVar, Generic

T = TypeVar('T')

class Stack(Generic[T]):
    def __init__(self):
        self._items: list[T] = []
    
    def push(self, item: T) -> None:
        self._items.append(item)
    
    def pop(self) -> T:
        return self._items.pop()

int_stack: Stack[int] = Stack()
int_stack.push(10)
int_stack.push(20)
print(int_stack.pop())
# 20

Protocol

Protocol defines structural subtyping interfaces without inheritance:

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(d: Drawable) -> None:
    d.draw()

render(Circle())
# Drawing circle
render(Square())
# Drawing square

Optional and Union

Optional and Union describe types that can be one of multiple options:

from typing import Optional, Union

def greet(name: Optional[str]) -> str:
    if name is None:
        return "Hello, stranger!"
    return f"Hello, {name}!"

print(greet(None))
# Hello, stranger!
print(greet("Alice"))
# Hello, Alice!

def process(value: Union[int, str]) -> str:
    return str(value)

print(process(42))
# 42
print(process("hello"))
# hello

Common Type Aliases

List, Dict, Tuple

These generic types specify container contents:

from typing import List, Dict, Tuple, Set

names: List[str] = ["Alice", "Bob", "Charlie"]
ages: Dict[str, int] = {"Alice": 30, "Bob": 25}
coordinates: Tuple[int, int] = (10, 20)
unique_ids: Set[int] = {1, 2, 3}

print(names)
# ['Alice', 'Bob', 'Charlie']
print(ages)
# {'Alice': 30, 'Bob': 25}

Callable

Callable represents callable objects (functions):

from typing import Callable

def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
    return func(a, b)

def multiply(x: int, y: int) -> int:
    return x * y

result = apply(multiply, 3, 4)
print(result)
# 12

Literal

Literal restricts a value to specific literal values:

from typing import Literal

def set_status(status: Literal["pending", "approved", "rejected"]) -> None:
    print(f"Status set to: {status}")

set_status("pending")
# Status set to: pending
set_status("approved")
# Status set to: approved

Type Guards and Cast

TypeGuard

TypeGuard narrows types within conditional blocks:

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):
        print(" ".join(items))
    else:
        print("Not all strings")

process(["a", "b", "c"])
# a b c
process([1, 2, "x"])
# Not all strings

cast

cast tells the type checker to treat a value as a different type:

from typing import cast

def unsafe_parse(data: dict[str, object]) -> list[int]:
    items = data["items"]
    return cast(list[int], items)

result = unsafe_parse({"items": [1, 2, 3]})
print(result)
# [1, 2, 3]

Common Patterns

Type Checking Functions

Use isinstance() and type guards to help type checkers:

from typing import TypeGuard

def is_dict(val: object) -> TypeGuard[dict]:
    return isinstance(val, dict)

def process(val: object) -> None:
    if is_dict(val):
        print(val.keys())

Overload Functions

Use @overload for multiple function signatures:

from typing import overload, Union

@overload
def process(value: int) -> int: ...
@overload
def process(value: str) -> str: ...
@overload
def process(value: list[int]) -> list[int]: ...

def process(value: Union[int, str, list[int]]) -> Union[int, str, list[int]]:
    if isinstance(value, list):
        return [x * 2 for x in value]
    if isinstance(value, str):
        return value.upper()
    return value * 2

print(process(5))
# 10
print(process("hello"))
# HELLO
print(process([1, 2, 3]))
# [2, 4, 6]

Final and Read-Only

Use Final for constants and ReadOnly for immutable typed dict keys:

from typing import Final, TypedDict, ReadOnly

MAX_SIZE: Final = 1000

class Config(TypedDict):
    name: str
    value: ReadOnly[int]

config: Config = {"name": "app", "value": 42}

See Also