Type Hinting ============ As of version 2.0.0, **wrapt** includes type hints for its public APIs to improve interoperability with static type checkers (e.g. ``pyright``, ``mypy``). The type metadata is available when running on Python 3.10 or later (the minimum version the annotations target). The type annotations aim to ensure type inference works correctly in the common cases, but the dynamic nature of decorators and wrappers means that some patterns cannot be expressed precisely. The type hints are designed to be broadly compatible with the major type checkers, but there are some differences in how they interpret certain constructs, so you may find that some things work in one type checker but not another. In some situations you may still need to add explicit annotations yourself, sometimes by introducing small helper functions to guide the type checker. Of the type checkers, ``pyright`` used within VS Code Pylance extension gives the best experience. ``mypy`` and ``ty`` also work well in most situations, but have some limitations when dealing with static methods of classes decorated with **wrapt** decorators. The ``pyrefly`` type checker currently does not work correctly with **wrapt** decorators applied to methods of classes. Problems with ``mypy``, ``ty`` and ``pyrefly`` may be due to limitations in those type checkers rather than issues with the **wrapt** type hints. Many attempts have been made to ensure compatibility, but it is not possible to guarantee that all type checkers will work correctly in all situations due to differences in how they interpret type hint constructs. Always ensure you are using the most up to date version of a type checking tool. If you encounter any issues, please report them on the **wrapt** issue tracker so they can be investigated and suggested workarounds or fixes can be provided. Function Decorators ------------------- The **wrapt** module exposes two helpers for authoring function decorators: ``@decorator`` and ``@function_wrapper``. ``@function_wrapper`` is a lightweight subset of ``@decorator`` intended for straightforward function wrapping; it omits the advanced extension points in exchange for a smaller, simpler surface. We will use ``@decorator`` in this discussion of type hints, but ``@function_wrapper`` is also fully type hinted and can be used in a similar way. The most basic example of a function decorator is: :: @wrapt.decorator def pass_through(wrapped, instance, args, kwargs): return wrapped(*args, **kwargs) @pass_through def add(a, b): return a + b result = add(2, 3) In this example, because no type hints are provided, the type checker will not be able to infer the types of the function's parameters or return value. Adding type hints, the example becomes: :: from typing import Any, Callable, ParamSpec, TypeVar P = ParamSpec("P") R = TypeVar("R") @wrapt.decorator def pass_through( wrapped: Callable[P, R], instance: Any, args: tuple[Any, ...], kwargs: dict[str, Any], ) -> R: return wrapped(*args, **kwargs) @pass_through def add(a: int, b: int) -> int: return a + b result: int = add(2, 3) In this example, because the decorator simply passes through the call to the wrapped function and is intended to be generic, it is necessary to use ``ParamSpec`` and ``TypeVar`` to express that the decorator can be applied to functions with any signature and return type. The ``wrapped`` parameter is annotated with ``Callable[P, R]`` to indicate that it accepts the same parameters as the decorated function and returns the same type. The ``instance``, ``args``, and ``kwargs`` parameters are annotated with appropriate generic types. The return type of the decorator is annotated as ``R`` to match the return type of the wrapped function. With the type hints as shown, the type checker should be able to validate both the arguments supplied to the decorated function and its return value at the point it is being called. For example, the type checker will flag incorrect argument types or an incompatible assigned result as in the following. :: result: str = add("hello", "world") # Error: incompatible types When ``@decorator`` or ``@function_wrapper`` are applied to a wrapper function, the type checker should give an error when the return type of the wrapper function is incompatible with the returned type given for the wrapped function. The type checker will in some circumstances not appear to correctly verify that the number and type of arguments of the wrapper function itself are what is expected because of the decorators being able to be applied to a class, a class instance, instance methods, class methods and normal functions. Thus there are multiple valid signatures for the wrapper function depending on the context in which the decorator is applied. As such, it may look like it does not flag incorrect arguments as it matches a different but still valid signature. No type checking is performed when the custom decorator is applied to a function. This is because the decorator can be applied to any callable with any signature, and the type checker cannot determine the correct signature to use for the decorated function. Signature Adapters ------------------ .. note:: The ``adapter`` argument described in this section is planned for deprecation. New code should instead use the ``wrapt.with_signature`` decorator, which provides the same signature override capability as a standalone decorator that composes cleanly with ``wrapt.decorator`` and other wrapt decorators. See :doc:`bundled` for details. Sometimes you want the decorated callable to present a different public signature from the underlying implementation (for example, to narrow the parameters, rename them, or enforce keyword-only usage). You can express this using a signature adapter: a small prototype function whose only purpose is to declare the outward-facing signature the wrapped function should appear to have after decoration. For instance, imagine the original function returns an integer, but you want the decorated function to return a string. You would define a signature adapter like this: :: def adapter_prototype(i: int) -> str: ... @wrapt.decorator(adapter=wrapt.adapter_factory(adapter_prototype)) def int_to_str(wrapped, instance, args, kwargs): return str(wrapped(*args, **kwargs)) @int_to_str def function(x) -> int: """A function that takes an integer and returns it.""" return x result = function(1) In this example we passed the prototype function itself via the ``adapter`` argument. **wrapt** also supports alternative forms: you can supply the prototype as a string, or return a pre-formatted argument spec instead of a callable. Declaring the adapter explicitly ensures that runtime introspection (``inspect.signature``, ``help()``, IDE tooling, etc.) reports the adapted signature rather than the underlying implementation detail. Because the adaptation is applied dynamically (and the prototype may itself be generated at runtime), the **wrapt** type hints will not work, and so you must use a helper function. :: def adapter_prototype(i: int) -> str: ... def int_to_str(wrapped: Callable[[int], int]) -> Callable[[int], str]: @wrapt.decorator(adapter=adapter_prototype) def wrapper( wrapped: Callable[[int], int], instance: Any, args: tuple[Any, ...], kwargs: dict[str, Any], ) -> Any: return str(wrapped(*args, **kwargs)) return wrapper(wrapped) @int_to_str def function(x: int) -> int: """A function that takes an integer and returns it.""" return x result: str = function(1) In this version the outer helper function constructs the decorator and added explicit type hints to its parameters and return type. This allows the type checker to validate calls to the decorated function and propagate the correct return type. Note that inside the decorator body the ``wrapped`` callable is annotated with signature of the functions to be wrapped. The return type of the wrapper function does however need to be ``Any``. You could just as well omit those inner annotations as what matters for most static checking is the user facing signature exposed by the outer helper function. Decorating Classes ------------------ Decorators can be applied to classes as well as functions and methods. when applied to a class, the decorator object effectively replaces the original. With the way the **wrapt** decorator works, it is still possible to use the decorated class as a base class in an inheritance hierarchy, however, this confuses the type checker. :: @pass_through class BaseClass: def __init__(self): ... # Error: type checker doesn't recognise the class as a base class. class DerivedClass(BaseClass): # <-- Invalid error warning. def __init__(self): ... The type checker can also give invalid error warnings when using functions such as ``issubclass()`` due to not recognising the decorated class as a class type. :: # Error: type checker doesn't recognise the class as a base class. issubclass(DerivedClass, BaseClass) # <-- Invalid error warning. Class as Decorator ------------------ Normally decorators are functions, but it is also possible to use a class as a decorator. In this situation the wrapper function (``__call__()`` method of class) is not type checked as it would be if the ``@decorator`` were being applied to it directly. Further, the type checker cannot match the arguments for the constructor of the class at the point it it is being created. :: @wrapt.decorator class ClassDecorator: def __init__(self, arg: str): ... # Error: type checker will not check arguments of wrapper function. def __call__(self, wrapped, instance, args, kwargs): ... # <-- Not checked. # Error: type checker doesn't recognise arguments correctly. @ClassDecorator("string") # <-- Invalid error warning. def function(): ... Object Proxies -------------- In addition to the decorator factories, the **wrapt** module exposes a family of object proxy classes that can be used to build custom wrappers for arbitrary objects. The core classes are ``wrapt.BaseObjectProxy``, ``wrapt.ObjectProxy``, ``wrapt.AutoObjectProxy``, ``wrapt.LazyObjectProxy`` and ``wrapt.CallableObjectProxy``. Each is generic on a single type parameter that stands for the type of the wrapped object. ``wrapt.BaseObjectProxy`` is the recommended base for custom proxy subclasses; ``wrapt.ObjectProxy`` is retained for backward compatibility and is a thin subclass of ``BaseObjectProxy``. The discussion below applies equally to both. Annotating a proxy with the type of its wrapped value lets the type checker reason about ``proxy.__wrapped__``: :: import wrapt proxy: wrapt.ObjectProxy[int] = wrapt.ObjectProxy(5) value: int = proxy.__wrapped__ Subclassing a proxy with a specific wrapped type is also supported, and is the idiomatic way to define a proxy for a particular kind of object: :: from io import TextIOWrapper from typing import Any class FileProxy(wrapt.ObjectProxy[TextIOWrapper[Any]]): pass fp = FileProxy(open("/path/to/file")) wrapped_file: TextIOWrapper[Any] = fp.__wrapped__ What the type parameter does and does not propagate ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The type parameter only affects the ``__wrapped__`` attribute directly. Most other access through the proxy is intentionally typed as ``Any``, because the runtime forwarding depends on the wrapped object's interface, which the stubs cannot claim statically without sacrificing flexibility: * Attribute access via ``proxy.foo`` returns ``Any``, since the proxy's ``__getattr__`` forwards to the wrapped object. * Arithmetic and bitwise operators such as ``proxy + 1`` return ``Any`` because the result type depends on the wrapped value's implementation. * Container operations (``proxy[key]``, ``len(proxy)``, ...) return permissive types. * Context-manager usage ``with proxy as v:`` binds ``v`` as ``Any``, because the wrapped object's ``__enter__`` can return a value of any type (for example, ``threading.Lock.__enter__`` returns ``bool``, not the lock itself). If you need a statically typed view of a specific attribute or operation, access the underlying value via ``proxy.__wrapped__`` where it is typed as the wrapped type, or assign the result of an expression to a variable with an explicit annotation: :: proxy: wrapt.ObjectProxy[int] = wrapt.ObjectProxy(5) bits: int = proxy.__wrapped__.bit_length() # Inferred as int. Using a proxy class without a type parameter ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Writing ``wrapt.ObjectProxy`` on its own as a type annotation, without a ``[T]`` parameter, is equivalent to ``wrapt.ObjectProxy[Any]`` from the type checker's perspective. It is valid code, but the wrapped object's type is unknown and ``proxy.__wrapped__`` is typed as ``Any`` rather than a specific type: :: proxy: wrapt.ObjectProxy = wrapt.ObjectProxy(5) # ObjectProxy[Any]. value = proxy.__wrapped__ # Any. Strict type checker modes (for example ``mypy --strict``) flag the unparameterised form as missing type parameters, and require you to write ``wrapt.ObjectProxy[Any]`` explicitly if that is what you intend. Under default settings the two forms are interchangeable for type checking purposes, but subscripting with a concrete type is preferred whenever the wrapped type is known. Function Wrappers ----------------- ``wrapt.FunctionWrapper`` and ``wrapt.BoundFunctionWrapper`` are the runtime types produced by ``@wrapt.decorator`` and ``@wrapt.function_wrapper``. Both are generic on two parameters: a ``ParamSpec`` representing the wrapped callable's parameter signature, and a ``TypeVar`` representing its return type. In most cases you do not need to annotate a function wrapper at all. The decorator machinery infers the type parameters from the signature of the wrapped function, so a decorated function continues to appear to the type checker as a callable with the same arguments and return type: :: @pass_through def add(a: int, b: int) -> int: return a + b # Inferred as wrapt.FunctionWrapper[[int, int], int]. If you want to store a reference to a decorated function in a container, pass it to another function, or otherwise name the type explicitly, the subscripted form can be used: :: wrapped_add: wrapt.FunctionWrapper[[int, int], int] = add As with object proxies, writing ``wrapt.FunctionWrapper`` on its own is equivalent to ``wrapt.FunctionWrapper[Any, Any]``: a callable of any signature returning any type. The idiomatic spelling for "any signature" is ``wrapt.FunctionWrapper[..., Any]``, using ``...`` in the parameter-spec position. Strict type checker modes will ask for explicit parameters in either case. ``wrapt.BoundFunctionWrapper`` is the type produced when a ``FunctionWrapper`` is accessed via the descriptor protocol on an instance or class (for example, a decorated method accessed through ``self``). You rarely need to name this type directly; it is produced automatically and flows through type inference.