Python Callable Type Hint: Using Callable in Type Annotations
python callable type hint: Learn how to type hint callables in Python using Callable, Protocol, and lambda annotations, with practical examples and common pitfalls.
When you need to pass a function as an argument, store it in a data structure, or return it from another function, the python 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 falls back to Any, which disables type checking for the entire call chain. 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.
Callable also supports a bare Callable without argument types: Callable[..., ReturnType]. This accepts any callable that returns the specified type, regardless of its arguments. It is useful when you only care about the return type, but it sacrifices argument checking.
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. Without the annotation, the return type would be inferred as Callable[..., int] or Any depending on the tool, which is less precise.
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 does not directly support keyword arguments or *args/**kwargs in its type parameters. The only way to describe a callable that accepts arbitrary arguments is to use Callable[..., ReturnType]. For example, a decorator that preserves the signature of 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, you have two options: use a Protocol with an __call__ method that includes keyword arguments, or use overloads. 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 error is using Callable without the square brackets, like Callable alone, which is not a valid type and will cause a runtime error when used in annotations if evaluated.
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 using Callable for functions with default arguments. Callable does not encode defaults, so a function with a default parameter is still compatible with a Callable that requires that parameter. The type checker only checks the number and types of arguments, not whether they are optional. This is usually acceptable because callers can always provide the argument.
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 object itself is a simple class instance; 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, all annotations become strings and are not evaluated at definition time. This can delay the evaluation of Callable until you access __annotations__, which is fine for static checking but may affect tools that introspect annotations at runtime, such as some serialization libraries. In most cases, this is not a problem, but it is worth knowing when you build tools that rely on runtime annotation inspection.
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.