For simple decorators, use Callable
:
from typing import Callable, Any
from functools import wraps
def my_decorator(func: Callable[..., Any]) -> Callable[..., Any]:
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@my_decorator
def greet(name: str) -> str:
return f"Hello, {name}!"
To preserve the original function's type signature:
from typing import TypeVar, Callable, Any
from functools import wraps
F = TypeVar('F', bound=Callable[..., Any])
def preserve_signature(func: F) -> F:
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper # type: ignore
@preserve_signature
def add(x: int, y: int) -> int:
return x + y
ParamSpec
provides more precise typing for function parameters:
from typing import ParamSpec, TypeVar, Callable
from functools import wraps
P = ParamSpec('P')
T = TypeVar('T')
def log_calls(func: Callable[P, T]) -> Callable[P, T]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
return func(*args, **kwargs)
return wrapper
@log_calls
def calculate(x: int, y: int, operation: str = "add") -> int:
return x + y if operation == "add" else x - y
For parameterized decorators:
from typing import ParamSpec, TypeVar, Callable, Any
from functools import wraps
P = ParamSpec('P')
T = TypeVar('T')
def retry(max_attempts: int) -> Callable[[Callable[P, T]], Callable[P, T]]:
def decorator(func: Callable[P, T]) -> Callable[P, T]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise e
return func(*args, **kwargs) # This line won't be reached
return wrapper
return decorator
@retry(3)
def unreliable_function() -> str:
import random
if random.random() < 0.7:
raise ValueError("Random failure")
return "Success!"
from typing import ParamSpec, TypeVar, Callable, Any
from functools import wraps
P = ParamSpec('P')
T = TypeVar('T')
class TimingDecorator:
def __call__(self, func: Callable[P, T]) -> Callable[P, T]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
import time
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__} took {time.time() - start:.4f} seconds")
return result
return wrapper
timer = TimingDecorator()
@timer
def slow_function() -> None:
import time
time.sleep(1)
For decorating methods, consider the self
parameter:
from typing import ParamSpec, TypeVar, Callable, Any, cast
from functools import wraps
P = ParamSpec('P')
T = TypeVar('T')
def method_decorator(func: Callable[P, T]) -> Callable[P, T]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
print(f"Calling method {func.__name__}")
return func(*args, **kwargs)
return cast(Callable[P, T], wrapper)
class MyClass:
@method_decorator
def my_method(self, value: int) -> str:
return f"Value is {value}"
For more complex scenarios with generics:
from typing import TypeVar, Generic, Callable, ParamSpec
from functools import wraps
P = ParamSpec('P')
T = TypeVar('T')
U = TypeVar('U')
def transform_result(transform_func: Callable[[T], U]) -> Callable[[Callable[P, T]], Callable[P, U]]:
def decorator(func: Callable[P, T]) -> Callable[P, U]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> U:
result = func(*args, **kwargs)
return transform_func(result)
return wrapper
return decorator
@transform_result(str.upper)
def get_greeting(name: str) -> str:
return f"hello, {name}"
- Use
ParamSpec
for better parameter type preservation - Use
TypeVar
with bounds for maintaining return types @wraps
is essential for preserving metadata- Type checkers like mypy work better with these patterns
- Generic decorators are fully supported with proper typing