Suppose we have the following classes:
class Foo:
def __init__(self, method):
self.method = method
def __get__(self, instance, owner):
if instance is None:
return self
return self.method(instance)
class Bar:
@Foo
def do_something(self) -> int:
return 1
Bar().do_something # is 1
Bar.do_something # is Foo object
How to type hint __get__ and method correctly so that Pylance understands Bar().do_something is of the return type of do_something? (like a standard property)
You'll need to overload the __get__ method.
I do not use VSCode myself, but I tested the code below with MyPy and I would expect Pyright to infer the types correctly as well.
>=3.9To make this as flexible as possible, I would suggest making Foo generic in terms of
from collections.abc import Callable
from typing import Generic, TypeVar, Union, overload
from typing_extensions import Concatenate, ParamSpec, Self
T = TypeVar("T") # class using the descriptor
P = ParamSpec("P") # parameter specs of the decorated method
R = TypeVar("R") # return value of the decorated method
class Foo(Generic[T, P, R]):
method: Callable[Concatenate[T, P], R]
def __init__(self, method: Callable[Concatenate[T, P], R]) -> None:
self.method = method
@overload
def __get__(self, instance: T, owner: object) -> R: ...
@overload
def __get__(self, instance: None, owner: object) -> Self: ...
def __get__(self, instance: Union[T, None], owner: object) -> Union[Self, R]:
if instance is None:
return self
return self.method(instance)
Demo:
from typing import TYPE_CHECKING
class Bar:
@Foo
def do_something(self) -> int:
return 1
a = Bar().do_something
b = Bar.do_something
print(type(a), type(b)) # <class 'int'> <class '__main__.Foo'>
if TYPE_CHECKING:
reveal_locals()
Running MyPy over this gives the desired output:
note: Revealed local types are:
note: a: builtins.int
note: b: Foo[Bar, [], builtins.int]
NOTE: (thanks to @SUTerliakov for pointing some of this out)
>=3.10, you can import Concatenate and ParamSpec directly from typing and you can use the |-notation instead of typing.Union.>=3.11 you can import Self directly from typing as well, meaning you won't need typing_extensions at all.<3.9Without Concatenate, ParamSpec and Self we can still make Foo generic in terms of the return value of the decorated method:
from __future__ import annotations
from collections.abc import Callable
from typing import Generic, TypeVar, Union, overload
R = TypeVar("R") # return value of the decorated method
class Foo(Generic[R]):
method: Callable[..., R]
def __init__(self, method: Callable[..., R]) -> None:
self.method = method
@overload
def __get__(self, instance: None, owner: object) -> Foo[R]: ...
@overload
def __get__(self, instance: object, owner: object) -> R: ...
def __get__(self, instance: object, owner: object) -> Union[Foo[R], R]:
if instance is None:
return self
return self.method(instance)
MyPy output for the same demo script from above:
note: Revealed local types are:
note: a: builtins.int
note: b: Foo[builtins.int]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With