GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Calling `help` executes @classmethod @property decorated methods · Issue #89519 · python/cpython

Calling help executes @classmethod @property decorated methods #89519

New issue
Copy link
New issue
Copy link
Closed
Closed
Calling help executes @classmethod @property decorated methods#89519
Copy link
Assignees
rhettingerserhiy-storchaka
Labels
3.11only security fixesonly security fixestype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

Description

randolf-scholz
mannequin
opened on Oct 3, 2021
Issue body actions
BPO 45356
Nosy @rhettinger, @terryjreedy, @ambv, @berkerpeksag, @serhiy-storchaka, @wimglenn, @corona10, @pablogsal, @akulakov, @sirosen, @randolf-scholz, @AlexWaygood
PRs
  • gh-89519: fix incorrect access of class property in pydoc and inspect #29239
  • Files
  • classmethod_property.py
  • classmethod_property.py
  • ClassPropertyIdea.py
  • Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

    Show more details

    GitHub fields:

    assignee = 'https://github.com/rhettinger'
    closed_at = None
    created_at = <Date 2021-10-03.19:45:55.963>
    labels = ['type-bug', '3.11']
    title = 'Calling `help` executes @classmethod @property decorated methods'
    updated_at = <Date 2022-02-17.20:03:40.349>
    user = 'https://github.com/randolf-scholz'

    bugs.python.org fields:

    activity = <Date 2022-02-17.20:03:40.349>
    actor = 'rhettinger'
    assignee = 'rhettinger'
    closed = False
    closed_date = None
    closer = None
    components = []
    creation = <Date 2021-10-03.19:45:55.963>
    creator = 'randolf.scholz'
    dependencies = []
    files = ['50325', '50326', '50344']
    hgrepos = []
    issue_num = 45356
    keywords = ['patch']
    message_count = 23.0
    messages = ['403109', '403110', '403111', '403504', '403505', '403578', '403582', '403611', '403613', '403653', '403699', '403713', '405100', '405115', '405121', '405142', '405143', '406600', '406605', '406606', '407493', '407499', '413451']
    nosy_count = 13.0
    nosy_names = ['rhettinger', 'terry.reedy', 'grahamd', 'lukasz.langa', 'berker.peksag', 'serhiy.storchaka', 'wim.glenn', 'corona10', 'pablogsal', 'andrei.avk', 'sirosen0', 'randolf.scholz', 'AlexWaygood']
    pr_nums = ['29239']
    priority = 'normal'
    resolution = None
    stage = 'patch review'
    status = 'open'
    superseder = None
    type = 'behavior'
    url = 'https://bugs.python.org/issue45356'
    versions = ['Python 3.11']

    Linked PRs

    • gh-89519: Remove classmethod descriptor chaining, deprecated since 3.11 #110163

    Activity

    Next
    1. randolf-scholz commented on Oct 3, 2021

      @randolf-scholz
      randolf-scholzmannequin
      on Oct 3, 2021
      MannequinAuthor
      More actions

      I noticed some strange behaviour when calling help on a class inheriting from a class or having itself @classmethod @Property decorated methods.

      from time import sleep
      from abc import ABC, ABCMeta, abstractmethod
      
      class MyMetaClass(ABCMeta):
          @classmethod
          @property
          def expensive_metaclass_property(cls):
              """This may take a while to compute!"""
              print("computing metaclass property"); sleep(3)
              return "Phew, that was a lot of work!"
      
          
      class MyBaseClass(ABC, metaclass=MyMetaClass):
          @classmethod
          @property
          def expensive_class_property(cls):
              """This may take a while to compute!"""
              print("computing class property .."); sleep(3)
              return "Phew, that was a lot of work!"
          
          @property
          def expensive_instance_property(self):
              """This may take a while to compute!"""
              print("computing instance property ..."); sleep(3)
              return "Phew, that was a lot of work!"
      
      class MyClass(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]'

      Stack Overflow thread: https://stackoverflow.com/questions/69426309

    2. randolf-scholz
      added
      3.9 (EOL)end of lifeend of life
      3.10 (EOL)end of lifeend of life
      type-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
      on Oct 3, 2021
    3. randolf-scholz commented on Oct 3, 2021

      @randolf-scholz
      randolf-scholzmannequin
      on Oct 3, 2021
      MannequinAuthor
      More actions

      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!
      
    4. AlexWaygood commented on Oct 3, 2021

      @AlexWaygood
      AlexWaygood
      on Oct 3, 2021
      Member
      More actions

      See also: https://bugs.python.org/issue44904

    5. terryjreedy commented on Oct 8, 2021

      @terryjreedy
      terryjreedy
      on Oct 8, 2021
      Member
      More actions

      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.

    6. terryjreedy commented on Oct 8, 2021

      @terryjreedy
      terryjreedy
      on Oct 8, 2021
      Member
      More actions

      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 ...

    7. terryjreedy
      added
      3.11only security fixesonly security fixes
      and removed
      3.9 (EOL)end of lifeend of life
      3.10 (EOL)end of lifeend of life
      on Oct 8, 2021
    8. randolf-scholz commented on Oct 10, 2021

      @randolf-scholz
      randolf-scholzmannequin
      on Oct 10, 2021
      MannequinAuthor
      More actions

      @terry I think the problem boils down to the fact that @classmethod @property decorated methods end up not being real properties.

      Calling MyBaseClass.__dict__ reveals:

      mappingproxy({'__module__': '__main__',
                    'expensive_class_property': <classmethod at 0x7f893e95dd60>,
                    'expensive_instance_property': <property at 0x7f893e8a5860>,
                    '__dict__': <attribute '__dict__' of 'MyBaseClass' objects>,
                    '__weakref__': <attribute '__weakref__' of 'MyBaseClass' objects>,
                    '__doc__': None,
                    '__abstractmethods__': frozenset(),
                    '_abc_impl': <_abc._abc_data at 0x7f893fb98740>})

      Two main issues:

      1. 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)`
      1. expensive_class_property does not implement getter, setter, deleter. This seems to be the real dealbreaker, for example, if we do
      MyBaseClass.expensive_class_property = 2
      MyBaseClass().expensive_instance_property = 2

      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.

    9. randolf-scholz commented on Oct 10, 2021

      @randolf-scholz
      randolf-scholzmannequin
      on Oct 10, 2021
      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!!

      import math
      
      class TrigConst: 
          const = math.pi
          def __get__(self, obj, objtype=None):
              print("__get__ called")
              return self.const
          
          def __set__(self, obj, value):
              print("__set__ called")
              self.const = value
              
      
      class Trig:
          const = TrigConst()              # Descriptor instance
      Trig().const             # calls TrigConst.__get__
      Trig().const = math.tau  # calls TrigConst.__set__
      Trig.const               # calls TrigConst.__get__
      Trig.const = math.pi     # overwrites TrigConst attribute with float.
      Reacted by Ole Jørgen Brønner, 5j9 and Austin
    10. rhettinger commented on Oct 10, 2021

      @rhettinger
      rhettinger
      on Oct 10, 2021
      Contributor
      More actions

      I'm merging bpo-44904 into this one because they are related.

    11. 50 remaining items

    12. facebook-github-bot
      added a commit that references this issue on Dec 22, 2023

      gh-89519 & gh-113157 (Remove classmethod descriptor chaining, depreca…

      f508768
    13. ryan-duve
      added a commit that references this issue on Dec 26, 2023

      pythongh-113157 pythongh-89519: Fix method descriptors (pythongh-113233)

      278a514
    14. kulikjak
      added a commit that references this issue on Jan 22, 2024

      pythongh-113157 pythongh-89519: Fix method descriptors (pythongh-113233)

      b9f2300
    15. aisk
      added 2 commits that reference this issue on Feb 11, 2024

      pythongh-89519: Remove classmethod descriptor chaining, deprecated si…

      c63736d

      pythongh-113157 pythongh-89519: Fix method descriptors (pythongh-113233)

      ead59a8
    16. QMalcolm
      added 2 commits that reference this issue on Feb 13, 2024

      Refactor `from_resource` to no longer use generics

      0006ebd

      Move `SourceDefinition` to `dbt/artifacts` (#9543)

      8a1b927
    17. ReHoss commented on May 29, 2024

      @ReHoss
      ReHoss
      on May 29, 2024
      More actions

      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:

      import sys
      from typing import Generic, Optional, TypeVar
      
      if sys.version_info < (3, 9):
          from typing import Callable, Type
      else:
          from builtins import type as Type
          from collections.abc import Callable
      
      T = TypeVar("T")
      RT = TypeVar("RT")
      
      
      class classproperty(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] = func
      
          def __set_name__(self, owner: Type[T], name: str) -> None:
              # Update based on class context.
              self.__module__ = owner.__module__
              self.__name__ = name
              self.__qualname__ = owner.__qualname__ + "." + name
      
          def __get__(self, instance: Optional[T], owner: Optional[Type[T]] = None) -> RT:
              if owner is None:
                  owner = type(instance)
              return self.__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:

      import sys
      from types import MethodType
      from typing import Any, Callable, Generic, Optional, TypeVar, Union
      
      if sys.version_info < (3, 9):
          from typing import Type
      else:
          from builtins import type as Type
      
      if sys.version_info < (3, 11):
          Self = TypeVar("Self", bound="classproperty")
      else:
          from typing import Self
      
      T = TypeVar("T")
      
      
      class BoundClassDescriptor(Generic[T]):
      
          def __init__(
              self,
              owner: Type[T],
              descriptor: property,
              module: Optional[str],
              name: Optional[str],
              qualname: Optional[str],
          ) -> None:
              self.__doc__ = descriptor.__doc__
              self.__module__ = module
              self.__name__ = name
              self.__qualname__ = qualname
              self.__wrapped__: property = descriptor
      
              self.get: Callable[[], Any]
              self.set: Callable[[Any], object]
              self.delete: Callable[[], object]
      
              if descriptor.fget is not None:
                  self.get = MethodType(descriptor.fget, owner)
      
              if descriptor.fset is not None:
                  self.set = MethodType(descriptor.fset, owner)
      
              if descriptor.fdel is not None:
                  self.delete = MethodType(descriptor.fdel, owner)
      
      
      class classproperty(Generic[T]):
      
          def __init__(
              self,
              fget: Optional[Callable[[Type[T]], Any]] = None,
              fset: Optional[Callable[[Type[T], Any], object]] = None,
              fdel: Optional[Callable[[Type[T]], object]] = None,
              doc: Optional[str] = None,
          ) -> None:
              self.__module__ = None
              self.__name__ = None
              self.__qualname__ = None
              self.__wrapped__: property = property(fget, fset, fdel, doc)
      
          def __set_name__(self, owner: Type[T], name: str) -> None:
              self.__module__ = owner.__module__
              self.__name__ = name
              self.__qualname__ = owner.__qualname__ + "." + name
      
          def __get__(
              self, instance: Optional[T], owner: Optional[Type[T]] = None
          ) -> BoundClassDescriptor[T]:
              if owner is None:
                  owner = type(instance)
              return BoundClassDescriptor(
                  owner,
                  self.__wrapped__,
                  self.__module__,
                  self.__name__,
                  self.__qualname__,
              )
      
          def getter(self: Self, func: Callable[[Type[T]], Any]) -> Self:
              self.__wrapped__ = self.__wrapped__.getter(func)
              return self
      
          def setter(self: Self, func: Callable[[Type[T], Any], object]) -> Self:
              self.__wrapped__ = self.__wrapped__.setter(func)
              return self
      
          def deleter(self: Self, func: Callable[[Type[T]], object]) -> Self:
              self.__wrapped__ = self.__wrapped__.deleter(func)
              return self

      Example:

      class A:
          _foo = 0
      
          @classproperty
          def foo(cls):
              """Description of foo class property."""
              return cls._foo
      
          @foo.setter
          def foo(cls, value):
              cls._foo = value
      
      
      class B(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.
      Help on BoundClassDescriptor in A:
      
      A.foo = <BoundClassDescriptor object>
          Description of foo class property.
      
      >>> help(A.foo.get)
      Help on method foo in module __main__:
      
      foo() method of builtins.type instance
          Description of foo class property.
      
      >>> help(A.foo.set)  # You could have custom setter documentation.
      Help on method foo in module __main__:
      
      foo(value) method of builtins.type instance
      
      >>> help(A.foo.delete)  # Only has a delete method if one was set.
      Traceback (most recent call last):
        ...
      AttributeError: 'BoundClassDescriptor' object has no attribute '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.

      Hi @SimpleArt ,

      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)

          def __get__(self, instance: Optional[T], owner: Type[T] | None = None) -> RT:
              if owner is None:
                  if instance is not None:
                      owner = type(instance)
                  else:
                      owner = None
              return self.__wrapped__(owner)  # pyright triggers warning

      also this assignment

      self.__module__ = func.__module__

      gives

      Cannot find reference 'module' in '(T) -> RT'

      Moreover, could you provide a simple example of using abstract class properties for Python>=3.13 ?

      Reacted by STofone
    18. Glyphack
      added 2 commits that reference this issue on Sep 2, 2024

      pythongh-89519: Remove classmethod descriptor chaining, deprecated si…

      e3d33d5

      pythongh-113157 pythongh-89519: Fix method descriptors (pythongh-113233)

      602f955
    19. millerdev
      added a commit that references this issue on Apr 8, 2025

      Class properties are deprecated in Python 3.11

      c37f543
    20. kaapstorm
      added a commit that references this issue on Apr 9, 2025

      Drop Repeater._repeater_type

      bcb3b2c
    21. STofone commented on May 10, 2025

      @STofone
      STofone
      on May 10, 2025
      More actions

      classproperty breaks the type checker.
      While Metaclass cannot be generic.
      Makes this code impossible to work fine

      from functools import cached_property
      from typing import Callable, Literal, LiteralString, Type
      
      
      class SetFunc[T: LiteralString]:
          def __init__(self, id: T):
              self.id = id
      
          def __call__(self, func: Callable):
              ...
              return func
      
      class META_A[T: LiteralString](type):
          @cached_property
          def set_func(cls) -> Type[SetFunc[T]]:
              return SetFunc
      
      class A[T: LiteralString](metaclass=META_A[T]):
          pass
      
      class B(A[Literal["1", "2", "3"]]):
          pass
      
      # type checking for id does not work
      @B.set_func("1")
      def func():
          pass

      python/mypy#11672

    22. mgeplf
      added 2 commits that reference this issue on Jun 4, 2025

      Remove `classproperty`, as it doesn't perform what it should

      8624fef

      Remove `classproperty`, as it doesn't work as expected (#304)

      fbdf1ca
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Metadata

    Metadata

    Assignees

    • @rhettinger
      rhettinger
    • @serhiy-storchaka
      serhiy-storchaka

    Labels

    3.11only security fixesonly security fixestype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      You are visitor 01402503 — GeezerVison does not store the pages you look up.
      Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.