1

Following suggestions here, I've implemented a classproperty decorator in Python as follows

class classproperty[T]:
    def __init__(self, func: Callable[..., T]):
        self.func = func

    def __get__(self, _obj: Any, owner: Any) -> T:
        return self.func(owner)

My question is whether we can do any better with the typing? _obj is, as I understand it, an instance of the class, and owner is the class itself, so something like

class classproperty[T, C]:
    def __init__(self, func: Callable[[type[C]], T]):
        self.func = func

    def __get__(self, _obj: C, owner: type[C]) -> T:
        return self.func(owner)

should maybe work, though mypy (1.15.0, Python version 3.13.1) is giving me, for the following code snippet

class Test:
    @classproperty
    def my_property(cls):
        return 1

the following error

error: Argument 1 to "classproperty" has incompatible type "Callable[[Test], Any]"; expected "Callable[[type[Never]], Any]"  [arg-type]

I'm not sure I understand this -- why is mypy expecting a function which takes in the Never type?

2
  • 1
    the annotations in your example look neat. I'd suggest you use this very example to file a bug against mypy itself - I suppose they don t take into account __get__ being called in a class in their static analysis.
    – jsbueno
    Commented 3 hours ago
  • My first guess was that there's no way for mypy to infer what type cls has from my_proprety itself. Adding an explicit notation like type['Test'] to cls would seem to help, but really just changes the error message. I suspect you need to modify mypy itself (say, via a plugin) for something like this to work properly (just like classmethod itself requires special mypy support.)
    – chepner
    Commented 2 hours ago

1 Answer 1

0

An alternative implementation can be found in this github repo, code taken with minor modifications:

Essentially here the trick is to inherit property and calling the function with the instance's class instead of the instance.

from typing import Callable, Generic, overload, Any

class classproperty[T_class, T_value](property):
    """
    Decorator for a Class-level property.

    Credit to Denis Rhyzhkov on Stackoverflow: https://stackoverflow.com/a/13624858
    """

    def __init__(self, func: "Callable[..., T_value]", /) -> None:
        """Initialise the classproperty object."""
        super().__init__(func)

    def __get__(self, owner_self: object, owner_cls: "type | None" = None, /) -> T_value:
        """Retrieve the value of the property."""
        if self.fget is None:
            BROKEN_OBJECT_MESSAGE = "Broken object 'classproperty'."
            raise RuntimeError(BROKEN_OBJECT_MESSAGE)
        value: T_value = self.fget(owner_cls)
        return value
    
class Test:
    @classproperty
    def my_property(cls) -> int:
        reveal_type(cls)
        return 1
        
reveal_type(Test.my_property)  # int
reveal_type(Test().my_property) # int

Below some further variants that you can use. Note you can use the Any Trick to manually type cls as the correct type. Do not decorate it as a classmethod as self.func with func a classmethod is not callable. Alternative classproperty3 can also remove the type restriction from the first parameter.

Note, how I did not make C a generic for the class but only for the function. This also circumvents the Never problem. I am not yet sure if this is a bug, or if mypy infers that no type can be passed and therefore it is Never.

class classproperty2[T, C]:
    def __init__(self, func: Callable[[type[C]], T]):
        # allows usage of @classmethod for typing-reasons
        if isinstance(func, classmethod):
            func = func.__func__  # cannot call classmethods
        self.func = func

    def __get__(self, _obj: C | None, owner: type[C]) -> T:
        return self.func(owner)
        
class classproperty3[T]:
    def __init__(self, func: Callable[..., T]):
        if isinstance(func, classmethod):
            func = func.__func__  # cannot call classmethods
        self.func = func

    def __get__[C](self, _obj: C  | None, owner: type[C] | None) -> T:
        return self.func(owner)
        

class Test2:
    @classproperty2  # Use the Any Trick
    def my_property1(cls: type["Test2"] | Any) -> int:
        reveal_type(cls)
        return 1
    
    @classproperty2
    @classmethod  # Only for typing reasons; needs to be stripped!
    def my_property2(cls) -> int:
        reveal_type(cls)
        return 1
        
    @classproperty3
    def my_property3(cls) -> int:
        reveal_type(cls)
        return 1
        
        
reveal_type(Test2.my_property1)
reveal_type(Test2().my_property1)

reveal_type(Test2.my_property2)
reveal_type(Test2().my_property2)

reveal_type(Test2.my_property3)
reveal_type(Test2().my_property3)
2
  • Adding the classmethod decorator "fixes" the type-checking, but breaks it at runtime, because classmethod objects are not callable.
    – chepner
    Commented 2 hours ago
  • Once more I forgot about this. Thanks. Edited and added warnings.
    – Daraan
    Commented 1 hour ago

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.