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]:
- A list of types corresponding to the positional arguments expected by the callable.
- 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 wrappertyping.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.