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:

  1. from typing import Any, Callable, TypeVar, Tuple, cast
  2.  
  3. F = TypeVar('F', bound=Callable[..., Any])
  4.  
  5. # A decorator that preserves the signature.
  6. def my_decorator(func: F) -> F:
  7. def wrapper(*args, **kwds):
  8. print("Calling", func)
  9. return func(*args, **kwds)
  10. return cast(F, wrapper)
  11.  
  12. # A decorated function.
  13. @my_decorator
  14. def foo(a: int) -> str:
  15. return str(a)
  16.  
  17. a = foo(12)
  18. reveal_type(a) # str
  19. 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:

  1. from typing import Any, Callable, TypeVar
  2.  
  3. F = TypeVar('F', bound=Callable[..., Any])
  4.  
  5. def route(url: str) -> Callable[[F], F]:
  6. ...
  7.  
  8. @route(url='/')
  9. def index(request: Any) -> str:
  10. return 'Hello world'

Sometimes the same decorator supports both bare calls and calls with arguments. This can beachieved by combining with @overload:

  1. from typing import Any, Callable, TypeVar, overload
  2.  
  3. F = TypeVar('F', bound=Callable[..., Any])
  4.  
  5. # Bare decorator usage
  6. @overload
  7. def atomic(__func: F) -> F: ...
  8. # Decorator with arguments
  9. @overload
  10. def atomic(*, savepoint: bool = True) -> Callable[[F], F]: ...
  11.  
  12. # Implementation
  13. def atomic(__func: Callable[..., Any] = None, *, savepoint: bool = True):
  14. def decorator(func: Callable[..., Any]):
  15. ... # Code goes here
  16. if __func is not None:
  17. return decorator(__func)
  18. else:
  19. return decorator
  20.  
  21. # Usage
  22. @atomic
  23. def func1() -> None: ...
  24.  
  25. @atomic(savepoint=False)
  26. def func2() -> None: ...