Table Of Contents
- Introduction
- Understanding typing.Callable
- Advanced Callable Patterns
- Typing Decorators
- Callback Functions and Event Handlers
- Higher-Order Functions
- Practical Applications
- Type Checking and Validation
- FAQ
- Conclusion
Introduction
Type hints in Python have revolutionized code clarity and maintainability, and typing.Callable
is one of the most powerful tools for annotating functions that accept or return other functions. Whether you're working with callbacks, decorators, or higher-order functions, proper function type hints make your code more readable and catch errors early.
In this comprehensive guide, we'll explore typing.Callable
, learn how to type various function patterns, and discover best practices for creating robust, well-typed Python applications.
Understanding typing.Callable
typing.Callable
is used to annotate callable objects - primarily functions, but also any object that implements the __call__
method. It specifies both the parameter types and return type of the callable.
Basic Callable Syntax
from typing import Callable
# Basic syntax: Callable[[param_types], return_type]
def process_data(data: list[str],
processor: Callable[[str], str]) -> list[str]:
"""Process each item in data using the processor function."""
return [processor(item) for item in data]
def uppercase(text: str) -> str:
return text.upper()
def add_prefix(text: str) -> str:
return f"PREFIX: {text}"
# Usage
data = ["hello", "world", "python"]
result1 = process_data(data, uppercase)
result2 = process_data(data, add_prefix)
print(result1) # ['HELLO', 'WORLD', 'PYTHON']
print(result2) # ['PREFIX: hello', 'PREFIX: world', 'PREFIX: python']
Callable with Multiple Parameters
from typing import Callable
def apply_operation(x: int, y: int,
operation: Callable[[int, int], int]) -> int:
"""Apply a binary operation to two integers."""
return operation(x, y)
def add(a: int, b: int) -> int:
return a + b
def multiply(a: int, b: int) -> int:
return a * b
def power(base: int, exponent: int) -> int:
return base ** exponent
# Usage
result1 = apply_operation(5, 3, add) # 8
result2 = apply_operation(5, 3, multiply) # 15
result3 = apply_operation(5, 3, power) # 125
Advanced Callable Patterns
Optional and Union Types with Callable
from typing import Callable, Optional, Union
def flexible_processor(data: list[str],
transformer: Optional[Callable[[str], str]] = None,
validator: Union[Callable[[str], bool], None] = None) -> list[str]:
"""Process data with optional transformer and validator functions."""
result = []
for item in data:
# Apply transformation if provided
processed_item = transformer(item) if transformer else item
# Apply validation if provided
if validator is None or validator(processed_item):
result.append(processed_item)
return result
def is_long_enough(text: str) -> bool:
return len(text) >= 5
def capitalize_words(text: str) -> str:
return text.title()
# Usage examples
data = ["hello", "hi", "python", "code"]
# Only transformation
result1 = flexible_processor(data, transformer=capitalize_words)
print(result1) # ['Hello', 'Hi', 'Python', 'Code']
# Only validation
result2 = flexible_processor(data, validator=is_long_enough)
print(result2) # ['hello', 'python']
# Both transformation and validation
result3 = flexible_processor(data,
transformer=capitalize_words,
validator=is_long_enough)
print(result3) # ['Hello', 'Python']
Generic Callable Types
from typing import Callable, TypeVar, Generic
T = TypeVar('T')
U = TypeVar('U')
def map_transform(items: list[T],
transform: Callable[[T], U]) -> list[U]:
"""Generic function to transform a list of items."""
return [transform(item) for item in items]
def filter_items(items: list[T],
predicate: Callable[[T], bool]) -> list[T]:
"""Generic function to filter items based on a predicate."""
return [item for item in items if predicate(item)]
# Usage with different types
numbers = [1, 2, 3, 4, 5]
strings = ["apple", "banana", "cherry"]
# Transform numbers to strings
str_numbers = map_transform(numbers, str)
print(str_numbers) # ['1', '2', '3', '4', '5']
# Transform strings to lengths
lengths = map_transform(strings, len)
print(lengths) # [5, 6, 6]
# Filter even numbers
even_numbers = filter_items(numbers, lambda x: x % 2 == 0)
print(even_numbers) # [2, 4]
Typing Decorators
Decorators are a common use case for Callable
type hints:
Simple Decorator Typing
from typing import Callable, TypeVar, Any
from functools import wraps
import time
F = TypeVar('F', bound=Callable[..., Any])
def timer(func: F) -> F:
"""Decorator to measure function execution time."""
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"{func.__name__} took {end_time - start_time:.4f} seconds")
return result
return wrapper # type: ignore
@timer
def slow_function(n: int) -> int:
"""A function that takes some time to execute."""
total = 0
for i in range(n):
total += i
return total
# Usage
result = slow_function(1000000)
Parameterized Decorator Typing
from typing import Callable, TypeVar, Any, Optional
from functools import wraps
F = TypeVar('F', bound=Callable[..., Any])
def retry(max_attempts: int = 3,
delay: float = 1.0,
exceptions: tuple[type[Exception], ...] = (Exception,)) -> Callable[[F], F]:
"""Decorator factory for retrying failed function calls."""
def decorator(func: F) -> F:
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
last_exception = None
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except exceptions as e:
last_exception = e
if attempt < max_attempts - 1:
time.sleep(delay)
print(f"Attempt {attempt + 1} failed, retrying...")
print(f"All {max_attempts} attempts failed")
raise last_exception
return wrapper # type: ignore
return decorator
@retry(max_attempts=3, delay=0.5, exceptions=(ValueError, TypeError))
def unreliable_function(x: int) -> str:
"""Function that might fail randomly."""
import random
if random.random() < 0.7: # 70% chance of failure
raise ValueError("Random failure")
return f"Success with {x}"
Callback Functions and Event Handlers
Callbacks are commonly used in event-driven programming and asynchronous operations:
from typing import Callable, Optional, Any, Protocol
import asyncio
# Protocol for event handlers
class EventHandler(Protocol):
def __call__(self, event_data: dict[str, Any]) -> None: ...
class EventManager:
"""Simple event manager with typed callbacks."""
def __init__(self) -> None:
self._handlers: dict[str, list[EventHandler]] = {}
def register(self, event_type: str,
handler: EventHandler) -> None:
"""Register an event handler."""
if event_type not in self._handlers:
self._handlers[event_type] = []
self._handlers[event_type].append(handler)
def emit(self, event_type: str, data: dict[str, Any]) -> None:
"""Emit an event to all registered handlers."""
handlers = self._handlers.get(event_type, [])
for handler in handlers:
handler(data)
def unregister(self, event_type: str,
handler: EventHandler) -> bool:
"""Unregister an event handler."""
if event_type in self._handlers:
try:
self._handlers[event_type].remove(handler)
return True
except ValueError:
pass
return False
# Event handlers
def user_login_handler(data: dict[str, Any]) -> None:
print(f"User {data['username']} logged in at {data['timestamp']}")
def security_audit_handler(data: dict[str, Any]) -> None:
print(f"Security audit: Login from IP {data.get('ip', 'unknown')}")
# Usage
event_manager = EventManager()
event_manager.register("user_login", user_login_handler)
event_manager.register("user_login", security_audit_handler)
event_manager.emit("user_login", {
"username": "alice",
"timestamp": "2025-01-15 10:30:00",
"ip": "192.168.1.100"
})
Async Callable Types
from typing import Callable, Awaitable, Any
import asyncio
# Type alias for async functions
AsyncFunc = Callable[..., Awaitable[Any]]
async def run_with_timeout(func: AsyncFunc,
timeout: float,
*args: Any,
**kwargs: Any) -> Any:
"""Run an async function with a timeout."""
try:
return await asyncio.wait_for(func(*args, **kwargs), timeout=timeout)
except asyncio.TimeoutError:
print(f"Function {func.__name__} timed out after {timeout} seconds")
raise
async def slow_async_operation(duration: float) -> str:
"""Simulate a slow async operation."""
await asyncio.sleep(duration)
return f"Completed after {duration} seconds"
# Usage
async def main() -> None:
try:
result = await run_with_timeout(slow_async_operation, 2.0, 1.5)
print(result) # Will complete successfully
result = await run_with_timeout(slow_async_operation, 1.0, 2.0)
print(result) # Will timeout
except asyncio.TimeoutError:
print("Operation timed out")
# asyncio.run(main())
Higher-Order Functions
Functions that return other functions benefit greatly from proper typing:
from typing import Callable, TypeVar, Any
T = TypeVar('T')
U = TypeVar('U')
def create_multiplier(factor: int) -> Callable[[int], int]:
"""Create a function that multiplies by a given factor."""
def multiplier(value: int) -> int:
return value * factor
return multiplier
def create_validator(predicate: Callable[[T], bool],
error_message: str) -> Callable[[T], T]:
"""Create a validator function based on a predicate."""
def validator(value: T) -> T:
if not predicate(value):
raise ValueError(error_message)
return value
return validator
def compose(f: Callable[[U], T],
g: Callable[[Any], U]) -> Callable[[Any], T]:
"""Compose two functions: f(g(x))."""
def composed(x: Any) -> T:
return f(g(x))
return composed
# Usage examples
double = create_multiplier(2)
triple = create_multiplier(3)
positive_validator = create_validator(
lambda x: x > 0,
"Value must be positive"
)
string_length_validator = create_validator(
lambda s: len(s) >= 3,
"String must be at least 3 characters"
)
# Function composition
def add_one(x: int) -> int:
return x + 1
def square(x: int) -> int:
return x * x
square_then_add_one = compose(add_one, square)
add_one_then_square = compose(square, add_one)
print(double(5)) # 10
print(square_then_add_one(3)) # 10 (3² + 1)
print(add_one_then_square(3)) # 16 ((3 + 1)²)
Practical Applications
Configuration and Dependency Injection
from typing import Callable, TypeVar, Generic, Any, Protocol
from dataclasses import dataclass
T = TypeVar('T')
class Factory(Protocol[T]):
"""Protocol for factory functions."""
def __call__(self) -> T: ...
@dataclass
class DatabaseConfig:
host: str
port: int
database: str
username: str
password: str
class Container:
"""Simple dependency injection container."""
def __init__(self) -> None:
self._factories: dict[type, Factory[Any]] = {}
self._singletons: dict[type, Any] = {}
def register_factory(self, type_: type[T],
factory: Factory[T]) -> None:
"""Register a factory for a type."""
self._factories[type_] = factory
def register_singleton(self, type_: type[T],
factory: Factory[T]) -> None:
"""Register a singleton factory for a type."""
def singleton_factory() -> T:
if type_ not in self._singletons:
self._singletons[type_] = factory()
return self._singletons[type_]
self._factories[type_] = singleton_factory
def get(self, type_: type[T]) -> T:
"""Get an instance of the specified type."""
if type_ not in self._factories:
raise ValueError(f"No factory registered for {type_}")
return self._factories[type_]()
# Usage
def create_database_config() -> DatabaseConfig:
return DatabaseConfig(
host="localhost",
port=5432,
database="myapp",
username="user",
password="password"
)
def create_database_connection() -> str:
"""Simulate database connection creation."""
return "Database connection established"
container = Container()
container.register_singleton(DatabaseConfig, create_database_config)
container.register_factory(str, create_database_connection)
config = container.get(DatabaseConfig)
connection = container.get(str)
Functional Programming Patterns
from typing import Callable, TypeVar, Iterable, Iterator
from functools import reduce
T = TypeVar('T')
U = TypeVar('U')
def pipe(*functions: Callable[[Any], Any]) -> Callable[[Any], Any]:
"""Create a pipeline of functions."""
return lambda x: reduce(lambda acc, f: f(acc), functions, x)
def curry2(func: Callable[[T, U], Any]) -> Callable[[T], Callable[[U], Any]]:
"""Convert a two-argument function to a curried function."""
def curried(first: T) -> Callable[[U], Any]:
def inner(second: U) -> Any:
return func(first, second)
return inner
return curried
def partial_apply(func: Callable[..., T], *args: Any) -> Callable[..., T]:
"""Partially apply arguments to a function."""
def partially_applied(*remaining_args: Any, **kwargs: Any) -> T:
return func(*args, *remaining_args, **kwargs)
return partially_applied
# Example usage
def add(a: int, b: int) -> int:
return a + b
def multiply(a: int, b: int) -> int:
return a * b
def square(x: int) -> int:
return x * x
# Create a pipeline: square, then add 5, then multiply by 2
pipeline = pipe(
square,
partial_apply(add, 5),
partial_apply(multiply, 2)
)
result = pipeline(3) # ((3² + 5) * 2) = 28
print(result)
# Curried functions
curried_add = curry2(add)
add_5 = curried_add(5)
print(add_5(10)) # 15
Type Checking and Validation
Runtime Type Checking
from typing import Callable, get_type_hints, Any
import inspect
def validate_callable(func: Callable[..., Any],
expected_signature: str) -> bool:
"""Validate that a callable matches expected signature."""
try:
sig = inspect.signature(func)
hints = get_type_hints(func)
# Simple validation logic
param_count = len(sig.parameters)
return_annotation = hints.get('return', Any)
print(f"Function: {func.__name__}")
print(f"Parameters: {param_count}")
print(f"Return type: {return_annotation}")
print(f"Expected: {expected_signature}")
return True # Simplified validation
except Exception as e:
print(f"Validation error: {e}")
return False
def safe_call(func: Callable[..., T],
*args: Any, **kwargs: Any) -> T | None:
"""Safely call a function and handle exceptions."""
try:
return func(*args, **kwargs)
except Exception as e:
print(f"Error calling {func.__name__}: {e}")
return None
# Example functions to validate
def add_numbers(a: int, b: int) -> int:
return a + b
def greet_user(name: str, prefix: str = "Hello") -> str:
return f"{prefix}, {name}!"
# Validation and safe calling
validate_callable(add_numbers, "int, int -> int")
result = safe_call(add_numbers, 5, 3)
print(f"Safe call result: {result}")
FAQ
Q: What's the difference between Callable and typing.Callable?
A: In Python 3.9+, you can use collections.abc.Callable
instead of typing.Callable
. They're functionally equivalent, but collections.abc.Callable
is preferred for newer code.
Q: How do I type a function that can accept any callable?
A: Use Callable[..., Any]
where ...
(Ellipsis) means any number and type of arguments.
Q: Can I use Callable with methods?
A: Yes, but be aware that bound methods include self
as the first parameter, which affects the type signature.
Q: How do I type lambda functions? A: Lambda functions are typed the same way as regular functions using their parameter and return types.
Q: What about typing functions with keyword-only arguments? A: Currently, Callable doesn't distinguish between positional and keyword-only arguments. Use Protocol for more precise typing.
Q: How do I type recursive functions?
A: Use forward references with strings: Callable[['MyType'], 'MyType']
or define the type after the class definition.
Conclusion
typing.Callable
is an essential tool for creating well-typed Python applications that work with functions as first-class objects. Key takeaways include:
- Use Callable for function parameters to specify expected signatures
- Combine with generics for flexible, reusable code
- Type decorators properly using TypeVar and bound constraints
- Consider Protocol for more complex callable requirements
- Test your type hints with mypy or similar tools
- Document complex signatures clearly for other developers
By mastering typing.Callable
, you'll write more robust, maintainable Python code that clearly communicates intent and catches errors early. This is especially valuable in larger codebases where functions are passed around frequently and type safety becomes crucial for maintainability.
Whether you're building event systems, implementing functional programming patterns, or creating sophisticated APIs, proper function typing with Callable will make your code more reliable and easier to understand.
Add Comment
No comments yet. Be the first to comment!