Python Callable Type Hint: Using Callable in Type Annotations
Learn how to type hint callables in Python using Callable and Protocol, with practical examples and common pitfalls for function annotations.
When you need to pass a function as an argument, store it in a data structure, or return it from another function, a callable type hint tells both humans and static type checkers what kind of function is expected. Without a proper annotation, a function parameter that accepts another function may be treated as Any, so type checkers cannot verify calls made through that parameter. The typing.Callable type is the standard way to describe a callable's signature, but it has limitations that become visible when you work with keyword arguments, overloads, or callable objects. This article explains how to use Callable correctly, when to reach for Protocol instead, and what happens at runtime when you annotate a callable.
What Is a Callable in Python?
A callable is any object that can be invoked with the () operator. Functions, lambdas, methods, and classes are all callable. Additionally, any instance of a class that defines __call__ is callable. For example:
class Adder: def __call__(self, a: int, b: int) -> int: return a + b add = Adder() result = add(2, 3) # 5
When you type hint a callable, you are describing the signature of that __call__ method (or the function itself). The Callable type from typing is the primary tool for this.
Using Callable from typing
The Callable type takes two parts: a list of argument types and a return type. The syntax is Callable[[arg1_type, arg2_type], return_type]. For a function that takes an int and a str and returns a bool, you write:
from typing import Callable def apply_predicate(value: int, predicate: Callable[[int, str], bool]) -> bool: return predicate(value, str(value))
Here, predicate is expected to accept two arguments, the first an int and the second a str, and return a bool. The type checker verifies that any function passed to apply_predicate matches that signature. If you pass a lambda that takes three arguments, mypy or Pyright will report an error.
For cases where you only need to constrain the return type, use Callable[..., ReturnType]. This accepts any callable that returns the specified type, regardless of its arguments.
Type Hinting Function Parameters and Return Values
The most common use of Callable is in function signatures. Consider a higher-order function that applies a transformation to a list:
from typing import Callable, List def transform(items: List[int], func: Callable[[int], int]) -> List[int]: return [func(item) for item in items]
You can pass a built-in function, a lambda, or a user-defined function as long as it takes one int and returns an int:
def double(x: int) -> int: return x * 2 print(transform([1, 2, 3], double)) print(transform([1, 2, 3], lambda x: x + 1))
When a function returns a callable, you annotate the return type similarly:
def make_multiplier(factor: int) -> Callable[[int], int]: def multiply(x: int) -> int: return x * factor return multiply
This tells the caller that the returned object is a function that takes an int and returns an int. Adding the explicit outer annotation also protects against accidental Any when the returned function's own parameters are unannotated.
Type Hinting Lambda Expressions
Lambdas themselves do not have explicit annotations. Their types are inferred from the context. When you assign a lambda to a variable, you can annotate the variable with Callable to give the lambda a type:
from typing import Callable add_one: Callable[[int], int] = lambda x: x + 1
This is useful when the lambda is passed to a function that expects a specific signature. However, if you use a lambda directly in a function call, the type checker infers its type from the expected parameter type, so explicit annotation is often unnecessary. For example:
def apply(func: Callable[[int], int], value: int) -> int: return func(value) result = apply(lambda x: x * 2, 5)
The lambda is checked against Callable[[int], int]. If it takes the wrong number of arguments or returns an incompatible type, the type checker flags it.
Callable with Variable Arguments and Keyword Arguments
Callable type parameters describe positional argument types; they do not express keyword-only parameters, parameter names, or a flexible *args/**kwargs signature. For a callable that accepts arbitrary arguments, use Callable[..., ReturnType]. For example, a decorator that accepts any function can use Callable[..., Any]:
from typing import Callable, Any def log_decorator(func: Callable[..., Any]) -> Callable[..., Any]: def wrapper(*args: Any, **kwargs: Any) -> Any: print("Calling function") return func(*args, **kwargs) return wrapper
This loses all argument type information. If you need precise typing for functions with keyword arguments, use a Protocol with an __call__ method that includes the keyword arguments. If the callable has multiple supported signatures, overload __call__ in the Protocol. The Protocol approach is more explicit and is covered in the next section.
Callable Protocols for Precise Signatures
When Callable cannot express the signature you need—for instance, when a callable accepts keyword-only arguments or has overloads—you can define a Protocol with a __call__ method. This is a structural subtyping approach: any callable that matches the method signature is considered compatible, even if it does not explicitly inherit from the protocol.
from typing import Protocol class StringProcessor(Protocol): def __call__(self, value: str, *, uppercase: bool = False) -> str: ... def process(processor: StringProcessor, text: str) -> str: return processor(text, uppercase=True) def my_processor(value: str, *, uppercase: bool = False) -> str: return value.upper() if uppercase else value print(process(my_processor, "hello"))
Here, StringProcessor describes a callable that takes a str and an optional keyword-only uppercase flag, returning a str. The function my_processor matches this signature, so it can be passed to process. Protocol also works with callable objects that define __call__, giving you a unified way to type hint both functions and instances.
Common Mistakes and Pitfalls
A frequent mistake is forgetting to import Callable from typing. In Python 3.9 and later, you can also use collections.abc.Callable as an equivalent, but typing.Callable remains the most common and works across versions. Another common source of confusion is writing bare Callable instead of a subscripted form. Bare Callable is valid and is equivalent to Callable[..., Any], but it does not describe the parameter list. If you intend to specify a signature, use a subscripted form such as Callable[[int], str].
A subtler issue is variance. Callable is covariant in the return type and contravariant in the argument types. This means a function that returns a subtype can be used where a function returning a supertype is expected, and a function that accepts a supertype can be used where a function accepting a subtype is expected. For example, Callable[[], int] is a subtype of Callable[[], object], but Callable[[object], None] is a subtype of Callable[[int], None]. Understanding this helps when passing functions to generic APIs.
Another pitfall is expecting Callable to encode default arguments. It does not. A function with an optional parameter can still satisfy a Callable that includes that parameter, because the function can accept the extra argument. Defaults are a property of the concrete function, not of the callback type.
Performance and Runtime Impact
Type hints, including Callable, are not enforced at runtime. They are used by static type checkers and IDEs. At runtime, the annotation is stored in the function's __annotations__ attribute, but it does not affect execution speed or memory usage in any meaningful way. The Callable annotation itself is a typing object; constructing it has negligible overhead. The real benefit is in development: type checkers catch mismatched function signatures before the code runs, reducing runtime errors and making refactoring safer.
There is one runtime consideration: if you use from __future__ import annotations, annotations are stored as strings and are not evaluated at definition time. This can affect tools that call typing.get_type_hints() or otherwise resolve annotations at runtime, because those callers must evaluate the strings before inspecting them. Static type checkers can still read the source annotations, so this is usually not a practical problem.
Compatibility and Tooling
typing.Callable has been available since Python 3.5. For Python 3.9 and later, collections.abc.Callable supports the same subscripting syntax and is preferred by some style guides. If you support Python 3.8 or earlier, stick with typing.Callable. Static type checkers like mypy, Pyright, and pytype all understand Callable and enforce its usage. The Protocol approach requires Python 3.8 or later (or typing_extensions for earlier versions).
When you use Callable in a codebase, ensure that your type checker is configured to check annotations. For mypy, you might need to set check_untyped_defs = True to catch errors in functions that lack annotations. The combination of Callable and Protocol covers nearly all callable typing needs, from simple function parameters to complex callback interfaces.
Choosing Between Callable and Protocol
Use Callable when the signature is simple and can be expressed with positional arguments and a single return type. It is concise and immediately readable. Use Protocol when you need keyword arguments, optional parameters, overloads, or when you want to give a descriptive name to a callback interface that appears in multiple places. Protocol also allows you to define multiple __call__ overloads, which Callable cannot do.
For example, an event handler that receives a Request and returns a Response is simple enough for Callable[[Request], Response]. But a callback that can be called with different argument patterns, such as an error handler that accepts either a string or an exception, is better expressed as a Protocol with overloads. The decision comes down to the complexity of the signature and how often the type is reused. If you find yourself repeating the same long Callable type in many places, extracting it into a Protocol improves maintainability and gives the type a clear name that documents its purpose.