13

In this pull request it looks like type hinting support for descriptors was added.

However it looks like no finalized "correct" usage example was ever posted, nor does it looks like any documentation was ever added to the typing module or to Mypy.

It looks like the correct usage is something like this:

from typing import TypeVar

T = TypeVar('T')
V = TypeVar('V')


class classproperty():
    def __init__(self, getter: Callable[[Type[T]], V]) -> None:
        self.getter = getter

    def __get__(self, instance: Optional[T], owner: Type[T]) -> V:
        return self.getter(owner)


def forty_two(cls: Type) -> int:
    return 42


class C:
    forty_two: int = classproperty(forty_two)

This seems logical, but I have no idea if that's actually the right way to do things.

Is there any documentation on this? Or complete examples that actually works on the version that was merged?

aaronsteers
  • 2,277
  • 2
  • 21
  • 38
shadowtalker
  • 12,529
  • 3
  • 53
  • 96
  • I'm not sure if there's necessarily any one correct way of typing a descriptor? The [descriptor protocol](https://docs.python.org/3/howto/descriptor.html#descriptor-protocol) is very flexible -- e.g. it would be valid for some descriptor to accept a generic instance + return a generic value, and for another to only accept some class Foo and always returns an int... If you're not sure how to type some descriptor, my advice is to first get it working at runtime, then add types after-the-fact. Once your runtime behavior is set, it ought to be easier to figure out what the appropriate types are. – Michael0x2a Jan 31 '19 at 02:03
  • @Michael0x2a so should I always use the return type of `__get__` as the type hint for the descriptor? I guess that's the heart of my question. – shadowtalker Jan 31 '19 at 02:44

3 Answers3

4

After some time of struggling with this issue, this page being the top result when you search for "type hinting descriptors", I would like to share a solution that fully satisfies the mypy and pyright static type checkers, is python 3.6 compatible and does not inherit from property

from typing import Callable, Generic, Type, TypeVar, overload, Union


Instance = TypeVar('Instance')
Value = TypeVar('Value')
Attribute = TypeVar('Attribute')


class Descriptor(Generic[Instance, Attribute, Value]):
    def __init__(self, method: Callable[[Instance, Attribute], Value]):
        """ Called on initialisation of descriptor """

    @overload
    def __get__(self, instance: None, owner: Type[Instance]) -> 'Descriptor':
        """ Called when an attribute is accessed via class not an instance """

    @overload
    def __get__(self, instance: Instance, owner: Type[Instance]) -> Value:
        """ Called when an attribute is accessed on an instance variable """

    def __get__(self, instance: Union[Instance, None], owner: Type[Instance]) -> Union[Value, 'Descriptor']:
        """ Full implementation is declared here """
        ...

    def __set__(self, instance: Instance, value: Value):
        """ Called when setting a value."""
        
serhiy1
  • 56
  • 2
3

The method described in the question seems to work for both Mypy and the PyCharm type checker.

Edit: Apparently my example code type checks, but MyPy cannot actually infer the type as of version 0.800.

"""Defines the `classproperty` decorator."""

from typing import Any, Callable, Optional, Type, TypeVar


T = TypeVar("T")
V = TypeVar("V")


class classproperty(property):
    """Class property decorator."""

    def __init__(self, getter: Callable[[Any], V]) -> None:
        """Override getter."""
        self.getter = getter  # type: ignore

    def __get__(self, instance: Optional[T], owner: Type[T]) -> V:  # type: ignore
        return self.getter(owner)  # type: ignore

    def __set__(self, obj, value):
        super(classproperty, self).__set__(type(obj), value)

    def __delete__(self, obj):
        super(classproperty, self).__delete__(type(obj))


class Thing:
    @classproperty
    def value1(cls) -> int:
        return 44

    value2 = classproperty(lambda cls: 55)

    @property
    def value3(self) -> int:
        return 66


thing = Thing()
reveal_type(thing.value1)
reveal_type(thing.value2)
reveal_type(thing.value3)
main.py:40: note: Revealed type is '<nothing>'
main.py:41: note: Revealed type is '<nothing>'
main.py:42: note: Revealed type is 'builtins.int'

https://mypy-play.net/?mypy=0.800&python=3.9&gist=79f6fab466ecc4c4b45b75f1c7b6a6a8.

shadowtalker
  • 12,529
  • 3
  • 53
  • 96
1

I could not get the example to work with MyPy. However, the derived definition below worked for me:

"""Defines the `classproperty` decorator."""

from typing import Any, Callable, Optional, Type, TypeVar

T = TypeVar("T")
V = TypeVar("V")


class classproperty(property):
    """Class property decorator."""

    def __init__(self, getter: Callable[[Any], V]) -> None:
        """Override getter."""
        self.getter = getter  # type: ignore

    def __get__(self, instance: Optional[T], owner: Type[T]) -> V:  # type: ignore
        return self.getter(owner)  # type: ignore

    def __set__(self, obj, value):
        super(classproperty, self).__set__(type(obj), value)

    def __delete__(self, obj):
        super(classproperty, self).__delete__(type(obj))

aaronsteers
  • 2,277
  • 2
  • 21
  • 38