Declaring decorators
One common application of type variable upper bounds is in declaring adecorator that preserves the signature of the function it decorates,regardless of that signature.
Note that class decorators are handled differently than function decorators inmypy: decorating a class does not erase its type, even if the decorator hasincomplete type annotations.
Here’s a complete example of a function decorator:
- from typing import Any, Callable, TypeVar, Tuple, cast
- F = TypeVar('F', bound=Callable[..., Any])
- # A decorator that preserves the signature.
- def my_decorator(func: F) -> F:
- def wrapper(*args, **kwds):
- print("Calling", func)
- return func(*args, **kwds)
- return cast(F, wrapper)
- # A decorated function.
- @my_decorator
- def foo(a: int) -> str:
- return str(a)
- a = foo(12)
- reveal_type(a) # str
- foo('x') # Type check error: incompatible type "str"; expected "int"
From the final block we see that the signatures of the decoratedfunctions foo()
and bar()
are the same as those of the originalfunctions (before the decorator is applied).
The bound on F
is used so that calling the decorator on anon-function (e.g. my_decorator(1)
) will be rejected.
Also note that the wrapper()
function is not type-checked. Wrapperfunctions are typically small enough that this is not a bigproblem. This is also the reason for the cast()
call in thereturn
statement in my_decorator()
. See Casts and type assertions.
Decorator factories
Functions that take arguments and return a decorator (also called second-order decorators), aresimilarly supported via generics:
- from typing import Any, Callable, TypeVar
- F = TypeVar('F', bound=Callable[..., Any])
- def route(url: str) -> Callable[[F], F]:
- ...
- @route(url='/')
- def index(request: Any) -> str:
- return 'Hello world'
Sometimes the same decorator supports both bare calls and calls with arguments. This can beachieved by combining with @overload
:
- from typing import Any, Callable, TypeVar, overload
- F = TypeVar('F', bound=Callable[..., Any])
- # Bare decorator usage
- @overload
- def atomic(__func: F) -> F: ...
- # Decorator with arguments
- @overload
- def atomic(*, savepoint: bool = True) -> Callable[[F], F]: ...
- # Implementation
- def atomic(__func: Callable[..., Any] = None, *, savepoint: bool = True):
- def decorator(func: Callable[..., Any]):
- ... # Code goes here
- if __func is not None:
- return decorator(__func)
- else:
- return decorator
- # Usage
- @atomic
- def func1() -> None: ...
- @atomic(savepoint=False)
- def func2() -> None: ...