You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Calling help executes @classmethod @property decorated methods #89519
I noticed some strange behaviour when calling help on a class inheriting from a class or having itself @classmethod@Property decorated methods.
fromtimeimportsleepfromabcimportABC, ABCMeta, abstractmethodclassMyMetaClass(ABCMeta):
@classmethod@propertydefexpensive_metaclass_property(cls):
"""This may take a while to compute!"""print("computing metaclass property"); sleep(3)
return"Phew, that was a lot of work!"classMyBaseClass(ABC, metaclass=MyMetaClass):
@classmethod@propertydefexpensive_class_property(cls):
"""This may take a while to compute!"""print("computing class property .."); sleep(3)
return"Phew, that was a lot of work!"@propertydefexpensive_instance_property(self):
"""This may take a while to compute!"""print("computing instance property ..."); sleep(3)
return"Phew, that was a lot of work!"classMyClass(MyBaseClass):
"""Some subclass of MyBaseClass"""help(MyClass)
Calling help(MyClass) will cause expensive_class_property to be executed 4 times (!)
The other two properties, expensive_instance_property and expensive_metaclass_property are not executed.
Secondly, only expensive_instance_property is listed as a read-only property; expensive_class_property is listed as a classmethod and expensive_metaclass_property is unlisted.
The problem is also present in '3.10.0rc2 (default, Sep 28 2021, 17:57:14) [GCC 10.2.1 20210110]'
I updated the script with dome more info. The class-property gets actually executed 5 times when calling help(MyClass)
Computing class property of <class '__main__.MyClass'> ...DONE!
Computing class property of <class '__main__.MyClass'> ...DONE!
Computing class property of <class '__main__.MyClass'> ...DONE!
Computing class property of <class '__main__.MyBaseClass'> ...DONE!
Computing class property of <class '__main__.MyClass'> ...DONE!
On current 3.9, 3.10, 3.11, on Windows running in IDLE, I see
computing class property ..
computing class property ..
computing class property ..
computing class property ..
computing class property ..
Help ...
Any analytics or documentation tool that has special treatment for properties may not identify 'expensive_class_property' as a property if they simply check via isinstance(func, property). Of course, this could be fixed by the tool-makers by doing a conditional check:
isinstance(func, property) or (`isinstance(func, classmethod)`and`isinstance(func.__func__, property)`
expensive_class_property does not implement getter, setter, deleter. This seems to be the real dealbreaker, for example, if we do
Then the first line erroneously executes, such that MyBaseClass.dict['expensive_class_property'] is now int instead of classmethod, while the second line correctly raises AttributeError: can't set attribute since the setter method is not implemented.
randolf-scholz commented on Oct 10, 2021
randolf-scholzmannequin
MannequinAuthor
More actions
If fact, in the current state it seem that it is impossible to implement real class-properties, for a simple reason:
descriptor.__set__ is only called when setting the attribute of an instance, but not of a class!!
In Python 3.10, classmethod() added a wrapped attribute. Presumably, any use case for implicit chaining can now be accomplished in an explicit and controlled manner.
@rhettinger - Could you share an example of how you would do this as a replacement for chaining @classmethod and @property?
@zduvall Here is an implementation for a read-only class property that is backwards/forwards compatible:
importsysfromtypingimportGeneric, Optional, TypeVarifsys.version_info< (3, 9):
fromtypingimportCallable, Typeelse:
frombuiltinsimporttypeasTypefromcollections.abcimportCallableT=TypeVar("T")
RT=TypeVar("RT")
classclassproperty(Generic[T, RT]):
""" Class property attribute (read-only). Same usage as @property, but taking the class as the first argument. class C: @classproperty def x(cls): return 0 print(C.x) # 0 print(C().x) # 0 """def__init__(self, func: Callable[[Type[T]], RT]) ->None:
# For using `help(...)` on instances in Python >= 3.9.self.__doc__=func.__doc__self.__module__=func.__module__self.__name__=func.__name__self.__qualname__=func.__qualname__# Consistent use of __wrapped__ for wrapping functions.self.__wrapped__: Callable[[Type[T]], RT] =funcdef__set_name__(self, owner: Type[T], name: str) ->None:
# Update based on class context.self.__module__=owner.__module__self.__name__=nameself.__qualname__=owner.__qualname__+"."+namedef__get__(self, instance: Optional[T], owner: Optional[Type[T]] =None) ->RT:
ifownerisNone:
owner=type(instance)
returnself.__wrapped__(owner)
Additionally, as shown in this issue thread, running help(...) on things like this will not work as expected. In the example shown in help(classproperty), running help(C) will show x = 0 instead of the documentation for the class property, if any. This is the same behavior as the current @classmethod @property solution, nothing fancy there. As such, you may want to simplify the __init__ and remove the __set_name__ as being completely unnecessary.
Note also that as shown by @Paebbels, the descriptor protocol ONLY supports class getters, it does not support class setters or class deleters. As such, only read-only class properties are possible.
This isn't to say you can't have mutable class properties however, only that you can't have them follow the descriptor protocol for the nicer syntax. You can instead have the class getter return objects with .get, .set, and .delete methods as a workaround:
classA:
_foo=0@classpropertydeffoo(cls):
"""Description of foo class property."""returncls._foo@foo.setterdeffoo(cls, value):
cls._foo=valueclassB(A):
pass
Usage:
>>># Getters/setters/deleters work as you'd expect using `A._foo` directly.>>>A.foo.get()
0>>>B.foo.get()
0>>>>>>A.foo.set(1)
>>>A.foo.get()
1>>>B.foo.get()
1>>>>>>B.foo.set(2)
>>>A.foo.get()
1>>>B.foo.get()
2>>>>>>help(A.foo) # Instance documentation works in Python >= 3.9.HelponBoundClassDescriptorinA:
A.foo=<BoundClassDescriptorobject>Descriptionoffooclassproperty.
>>>help(A.foo.get)
Helponmethodfooinmodule__main__:
foo() methodofbuiltins.typeinstanceDescriptionoffooclassproperty.
>>>help(A.foo.set) # You could have custom setter documentation.Helponmethodfooinmodule__main__:
foo(value) methodofbuiltins.typeinstance>>>help(A.foo.delete) # Only has a delete method if one was set.Traceback (mostrecentcalllast):
...
AttributeError: 'BoundClassDescriptor'objecthasnoattribute'delete'
Of course the documentation won't work on static analyzers, but you'll at least be able to dynamically use help(...) and get what you'd expect.
The pyright type checker for the instruction return self.__wrapped__(owner) returns:
Argument of type "type[T@classproperty] | type[object]* | None; cannot be assigned to parameter of type ";type[T@classproperty]"; Type ";type[T@classproperty] | type[object]* | None"; is incompatible with type ";type[T@classproperty]"; (reportArgumentType)
for this slightly modified version of the __get__ method (modified such that type(None) does not happen)
Activity
randolf-scholz commentedon Oct 3, 2021
I noticed some strange behaviour when calling
helpon a class inheriting from a class or having itself @classmethod @Property decorated methods.Calling
help(MyClass)will causeexpensive_class_propertyto be executed 4 times (!)The other two properties,
expensive_instance_propertyandexpensive_metaclass_propertyare not executed.Secondly, only
expensive_instance_propertyis listed as a read-only property;expensive_class_propertyis listed as a classmethod andexpensive_metaclass_propertyis unlisted.The problem is also present in '3.10.0rc2 (default, Sep 28 2021, 17:57:14) [GCC 10.2.1 20210110]'
Stack Overflow thread: https://stackoverflow.com/questions/69426309
randolf-scholz commentedon Oct 3, 2021
I updated the script with dome more info. The class-property gets actually executed 5 times when calling
help(MyClass)AlexWaygood commentedon Oct 3, 2021
See also: https://bugs.python.org/issue44904
terryjreedy commentedon Oct 8, 2021
Randolf, what specific behaviors do you consider to be bugs that should be fixed. What would a test of the the changed behavior look like?
This should perhaps be closed as a duplicate of bpo-44904. Randolf, please check and say what you thing.
terryjreedy commentedon Oct 8, 2021
On current 3.9, 3.10, 3.11, on Windows running in IDLE, I see
computing class property ..
computing class property ..
computing class property ..
computing class property ..
computing class property ..
Help ...
randolf-scholz commentedon Oct 10, 2021
@terry I think the problem boils down to the fact that
@classmethod @propertydecorated methods end up not being real properties.Calling
MyBaseClass.__dict__reveals:Two main issues:
isinstance(func, property). Of course, this could be fixed by the tool-makers by doing a conditional check:expensive_class_propertydoes not implementgetter,setter,deleter. This seems to be the real dealbreaker, for example, if we doThen the first line erroneously executes, such that MyBaseClass.dict['expensive_class_property'] is now
intinstead ofclassmethod, while the second line correctly raisesAttributeError: can't set attributesince the setter method is not implemented.randolf-scholz commentedon Oct 10, 2021
If fact, in the current state it seem that it is impossible to implement real class-properties, for a simple reason:
descriptor.__set__ is only called when setting the attribute of an instance, but not of a class!!
rhettinger commentedon Oct 10, 2021
I'm merging bpo-44904 into this one because they are related.
50 remaining items
gh-89519 & gh-113157 (Remove classmethod descriptor chaining, depreca…
pythongh-113157 pythongh-89519: Fix method descriptors (pythongh-113233)
pythongh-113157 pythongh-89519: Fix method descriptors (pythongh-113233)
pythongh-89519: Remove classmethod descriptor chaining, deprecated si…
pythongh-113157 pythongh-89519: Fix method descriptors (pythongh-113233)
Refactor `from_resource` to no longer use generics
Move `SourceDefinition` to `dbt/artifacts` (#9543)
ReHoss commentedon May 29, 2024
Hi @SimpleArt ,
The
pyrighttype checker for the instructionreturn self.__wrapped__(owner)returns:Argument of type "type[T@classproperty] | type[object]* | None; cannot be assigned to parameter of type ";type[T@classproperty]";
Type ";type[T@classproperty] | type[object]* | None"; is incompatible with type ";type[T@classproperty]"; (reportArgumentType)
for this slightly modified version of the
__get__method (modified such thattype(None)does not happen)also this assignment
gives
Cannot find reference 'module' in '(T) -> RT'
Moreover, could you provide a simple example of using abstract class properties for Python>=3.13 ?
pythongh-89519: Remove classmethod descriptor chaining, deprecated si…
pythongh-113157 pythongh-89519: Fix method descriptors (pythongh-113233)
Class properties are deprecated in Python 3.11
Drop Repeater._repeater_type
STofone commentedon May 10, 2025
classpropertybreaks the type checker.While Metaclass cannot be generic.
Makes this code impossible to work fine
python/mypy#11672
Remove `classproperty`, as it doesn't perform what it should
Remove `classproperty`, as it doesn't work as expected (#304)