Using typing.Callable for Python Function Signatures

Python's typing.Callable is a type hint used to indicate that an object can be called like a function, such as standard functions, lambdas, methods, or classes. It allows developers to document and statically verify the input parameter types and return types of higher-order functions and callbacks. This article explains the syntax of typing.Callable, how to specify argument lists and return types, how to handle arbitrary parameters, and how to address limitations regarding keyword arguments.

Basic Syntax and Structure

The Callable type accepts two arguments formatted as Callable[[ParamTypes], ReturnType]:

  1. A list of types corresponding to the positional arguments expected by the callable.
  2. The type of the value that the callable returns.
from typing import Callable

def execute_operation(x: int, y: int, operation: Callable[[int, int], int]) -> int:
    return operation(x, y)

def add(a: int, b: int) -> int:
    return a + b

result = execute_operation(5, 3, add)

In this example, Callable[[int, int], int] specifies that operation must accept exactly two integer arguments and return an integer.

Functions with No Arguments

If a function takes no arguments, provide an empty list [] as the first argument to Callable.

from typing import Callable

def run_task(task: Callable[[], None]) -> None:
    task()

def greet() -> None:
    print("Hello, world!")

run_task(greet)

The type annotation Callable[[], None] ensures that task receives no parameters and does not return a value.

Functions with Arbitrary Arguments

When you do not want to restrict the parameter types or number of arguments of a callable, use an ellipsis (...) instead of a list of types.

from typing import Callable

def log_execution(func: Callable[..., str]) -> None:
    result = func(1, 2, mode="verbose")
    print(result)

In this scenario, Callable[..., str] indicates that func can accept any number of positional or keyword arguments of any type, provided it returns a str.

Limitations with Keyword Arguments

A major limitation of typing.Callable is that it cannot specify keyword-only arguments, default parameter values, or variable positional arguments (such as *args and **kwargs) individually.

For example, Callable[[int, str], bool] indicates positional behavior; it does not inform a type checker whether those arguments have default values or can be passed by keyword.

Advanced Alternatives: ParamSpec and Protocol

To overcome the limitations of typing.Callable for complex function signatures, Python provides more expressive alternatives:

typing.ParamSpec (Python 3.10+)

ParamSpec captures the exact parameter signature of one function and transfers it to another, which is ideal for decorators.

from typing import Callable, ParamSpec, TypeVar

P = ParamSpec("P")
R = TypeVar("R")

def decorator(func: Callable[P, R]) -> Callable[P, R]:
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        return func(*args, **kwargs)
    return wrapper

typing.Protocol

For signatures requiring explicit keyword-only arguments or complex interfaces, define a class inheriting from typing.Protocol with a __call__ method.

from typing import Protocol

class ClickHandler(Protocol):
    def __call__(self, event_id: int, *, debug: bool = False) -> None:
        ...

def register_handler(handler: ClickHandler) -> None:
    handler(42, debug=True)

This protocol-based approach enables full structural subtyping for function signatures where typing.Callable syntax falls short.