Skip to content

Python Typing

The Python typing library provides support for type hints, enabling developers to specify the expected types of variables, function arguments, and return values. This improves code clarity, maintainability, and helps with static analysis tools like mypy.

The typing library in Python supports type hints for better code clarity and maintainability. Basic types such as int, str, float, and bool can be directly used.

def add(x: int, y: int) -> int:
return x + y

For container types, use List, Tuple, Dict, Set, etc.

from typing import List, Dict
def process_data(data: List[int]) -> Dict[str, int]:
return {"sum": sum(data)}

Use Optional for values that can be None.

from typing import Optional
def greet(name: Optional[str]) -> str:
return f"Hello, {name or 'Guest'}!"

Union specifies multiple possible types.

from typing import Union
def square(value: Union[int, float]) -> float:
return value ** 2

Callable specifies function types.

from typing import Callable
def operate(func: Callable[[int, int], int], x: int, y: int) -> int:
return func(x, y)

Use TypeVar for generics.

from typing import TypeVar, List
T = TypeVar('T')
def get_first(elements: List[T]) -> T:
return elements[0]

Create distinct types based on existing ones.

from typing import NewType
UserId = NewType('UserId', int)
def get_user_name(user_id: UserId) -> str:
return f"User{user_id}"

Use Literal to specify fixed values.

from typing import Literal
def move(direction: Literal['up', 'down', 'left', 'right']) -> str:
return f"Moving {direction}"

Protocol supports structural subtyping.

from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
class Circle:
def draw(self) -> None:
print("Drawing a circle")
def render(shape: Drawable) -> None:
shape.draw()

Annotated adds metadata to types.

from typing import Annotated
PositiveInt = Annotated[int, "A positive integer"]

Define dictionary types with fixed keys and value types.

from typing import TypedDict
class Point(TypedDict):
x: int
y: int
def print_point(p: Point) -> None:
print(f"x: {p['x']}, y: {p['y']}")

Final indicates that a value or method cannot be overridden or reassigned.

from typing import Final
PI: Final = 3.14159 # Cannot be reassigned
class Base:
def display(self) -> None:
pass
class Derived(Base):
def display(self) -> None: # This works
pass
@Final
def finalize(self) -> None:
pass
class SubDerived(Derived):
# def finalize(self) -> None: # Error: Cannot override
pass

Self specifies that a method returns an instance of the current class.

from typing import Self
class Builder:
def set_property(self, value: int) -> Self:
self.property = value
return self
def build(self) -> str:
return f"Built with property {self.property}"

Generics enable classes and functions to handle different types.

from typing import Generic, TypeVar
T = TypeVar('T')
class Box(Generic[T]):
def __init__(self, content: T) -> None:
self.content = content
def get_content(self) -> T:
return self.content
int_box = Box(123)
str_box = Box("hello")

Use @overload to define multiple signatures for a single function.

from typing import overload
@overload
def process(value: int) -> str: ...
@overload
def process(value: str) -> int: ...
def process(value):
if isinstance(value, int):
return str(value)
elif isinstance(value, str):
return len(value)

Use string literals for forward references, often necessary for self-referential types.

from typing import Union
class TreeNode:
def __init__(self, value: int, children: Union['TreeNode', None]) -> None:
self.value = value
self.children = children

Restricts a parameter to literal string values, often for security.

from typing import LiteralString
def execute_query(query: LiteralString) -> None:
print(f"Executing: {query}")

18. ParamSpec and Concatenate (Python 3.10+)

Section titled “18. ParamSpec and Concatenate (Python 3.10+)”

For higher-order functions that manipulate other functions’ signatures.

from typing import Callable, TypeVar, ParamSpec, Concatenate
P = ParamSpec('P')
R = TypeVar('R')
def add_logging(func: Callable[Concatenate[str, P], R]) -> Callable[P, R]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"Calling {func.__name__}")
return func("Logging enabled", *args, **kwargs)
return wrapper

Use type guards for runtime type narrowing.

from typing import TypeGuard, Union
def is_int_list(data: list[Union[int, str]]) -> TypeGuard[list[int]]:
return all(isinstance(x, int) for x in data)
values = [1, 2, 3]
if is_int_list(values):
print(sum(values))

Protocols enable “duck typing,” specifying only the necessary methods/attributes.

from typing import Protocol
class SupportsDraw(Protocol):
def draw(self) -> None: ...
class Square:
def draw(self) -> None:
print("Drawing a square")
def render(shape: SupportsDraw) -> None:
shape.draw()
render(Square())