Consider the following code: from typing import Any, Callable, Coroutine class Cache[**P, R]: @classmethod def decorate(cls, **params): def decorator(f: Callable[P, Coroutine[Any, Any, R]]) -> Callable[P, Coroutine[Any, Any, R]]: return f # in the real world, we instantiate a Cache here return decorator @Cache.decorate() async def some_function(i: int) -> int: return i + 1 cached_function = Cache.decorate()(some_function) If I ask pyright the type of Cache.decorate before the @classmethod wrapper (inspecting the word decorate in the above code), it returns: (method) def decorate( cls: type[Self@Cache[P@Cache, R@Cache]], **params: Unknown ) -> ((f: ((**P@Cache) -> (Coroutine[Any, Any, R@Cache])) -> ((**P@Cache) -> Coroutine[Any, Any, R@Cache])) That looks to me like it understands that P (the argument types) and R (the return types) are plumbed through correctly. However, if I ask it to introspect Cache.decorate in the context where it's being used as a decorator, it returns: (method) def decorate(**params: Unknown) -> ((f: ((...) -> Coroutine[Any, Any, Unknown])) -> ((...) -> Coroutine[Any, Any, Unknown])) ...which is to say, the relationship between input types and output types has been entirely discarded! Continue reading...