Consider writing an interface class in Python. The interface hides getter and setter methods behind properties. There are several properties that have a very similar structure and only differ by their name. To reduce code duplication, the interface uses a factory method for its properties:
from __future__ import annotations
class Interface:
def property_factory(name: str) -> property:
"""Create a property depending on the name."""
@property
def _complex_property(self: Interface) -> str:
# Do something complex with the provided name
return name
@_complex_property.setter
def _complex_property(self: Interface, _: str):
pass
return _complex_property
foo = property_factory("foo") # Works just like an actual property
bar = property_factory("bar")
def main():
interface = Interface()
interface.foo # Is of type '(variable) foo: Any' instead of '(property) foo: str'
if __name__ == "__main__":
main()
One issue with this implementation is that Interface.foo
and Interface.bar
will be marked as (variable) foo/bar: Any
, even though they should be (property) foo/bar: str
. Is there a way to type hint foo and bar properly?
Using an inline type hint like
foo: str = property_factory("foo")
feels misleading because foo is not really a string.
I'm not dead set on this property pattern. If there is a better way to create the properties I'd also be happy to change that. However, keep in mind that the interface requires properties as the real code does more complex stuff in the getter/setter methods. Also, I want to keep the properties instead of having a general method like get_property(self, name: str).
I can also share the actual code I'm working on, the above example is just a minimal example of what I have in mind.
Wild speculation: property_factory
is type hinted to return property
; the type of the decorated method/function is lost, which normally wouldn't happen since a type checker would also check that if it were used inside the body of a class.
property
is not generic (yet). However, the linked issue's OP themself provided a generic child class of property
as a possible solution. That was nearly 4 years ago when the type system is not anywhere as good as it currently is, so let's rewrite the class:
from typing import Any, Generic, TypeVar, overload, cast
T = TypeVar('T') # The return type
I = TypeVar('I') # The outer instance's type
class Property(property, Generic[I, T]):
def __init__(
self,
fget: Callable[[I], T] | None = None,
fset: Callable[[I, T], None] | None = None,
fdel: Callable[[I], None] | None = None,
doc: str | None = None
) -> None:
super().__init__(fget, fset, fdel, doc)
@overload
def __get__(self, instance: None, owner: type[I] | None = None) -> Callable[[I], T]:
...
@overload
def __get__(self, instance: I, owner: type[I] | None = None) -> T:
...
def __get__(self, instance: I | None, owner: type[I] | None = None) -> Callable[[I], T] | T:
return cast(Callable[[I], T] | T, super().__get__(instance, owner))
def __set__(self, instance: I, value: T) -> None:
super().__set__(instance, value)
def __delete__(self, instance: I) -> None:
super().__delete__(instance)
The rest of the methods can also be re-type-hinted this way, but that's an exercise for the reader. Now that we have a generic property
class, the original design can be rewritten as:
(The original works too, just use -> Property[Interface, str]
/return Property(_getter, _setter)
.)
from collections.abc import Callable
Getter = Callable[['Interface'], str]
Setter = Callable[['Interface', str], None]
def complex_property(name: str) -> tuple[Getter, Setter]:
def _getter(self: Interface) -> str:
...
def _setter(self: Interface, value: str) -> None:
...
return _getter, _setter
...which can then be used as:
class Interface:
foo = Property(*complex_property("foo"))
Assuming bar
is a normal @property
of the same class, here are some tests:
reveal_type(Interface.foo) # mypy => (Interface) -> str
# pyright => (Interface) -> str
reveal_type(Interface.bar) # mypy => (Interface) -> str
# pyright => property
reveal_type(instance.foo) # mypy + pyright => str
reveal_type(instance.bar) # mypy + pyright => str
instance.foo = 42 # mypy => error: Incompatible types in assignment
# pyright => error: "Literal[42]" is incompatible with "str" ('foo' is underlined)
instance.bar = 42 # mypy => error: Incompatible types in assignment
# pyright => error: "Literal[42]" is incompatible with "str" ('42' is underlined)
instance.foo = 'lorem' # mypy + pyright => fine
instance.bar = 'ipsum' # mypy + pyright => fine