Skip to content

MNT: Replace lazyproperty with cached_property in Python 3.8 #9036

Description

@pllim

As @Mariatta pointed out in #8881 (comment) , we might be able to replace lazyproperty with cached_property that is new in Python 3.8. We also need to figure out the best way to support this for Python<=3.7 or wait until our Python minversion is 3.8.

Activity

  1. bsipocz commented on Jul 22, 2019

    @bsipocz
    Member

    what about renaming our lazyproperty, so it'll be easier to switch it out once the time is here to require 3.8+?

  2. Mariatta commented on Jul 22, 2019

    @Mariatta

    Existing package you might want to look into if you're not using Python 3.8 yet: https://pypi.org/project/cached-property/

  3. pllim commented on Jul 22, 2019

    @pllim
    MemberAuthor

    @bsipocz , it is a possibility, though it would be more like alias than renaming, because we need to keep lazyproperty around for a while yet for backward compatibility. However, we do need to double check that they are indeed completely equivalent before making that alias.

  4. mhvk commented on Jul 22, 2019

    @mhvk
    Contributor

    Indeed, first check equivalence, but then we could follow a dual track of, under python >=3.8 doing effectively from functools import cached_property as lazyproperty and then deprecating lazyproperty once our minimum version is 3.8. Not that different than what we did with OrderedDict, etc.

  5. mhvk commented on Jul 22, 2019

    @mhvk
    Contributor

    Hmm, from a quick look, cached_property does not have the ability to have a setter or deleter - I definitely use the latter in my code. But perhaps we can upstream our implementations for those.

  6. astrofrog commented on Jul 24, 2019

    @astrofrog
    Member

    I don't think we should rename ours - I think we should just keep and then eventually deprecate/remove it once cached_property can be used instead. If we rename, we have to deprecate the old name, then deprecate the removal separately.

  7. embray commented on Jan 16, 2021

    @embray
    Member

    I'm generally in favor of this though ditto @mhvk that cached_property does not support custom setters and deleters, and lazyproperty's mechanism for setters is somewhat weird, but also useful, and is used in astropy.io.fits. However, it might be possible to re-implement most of lazyproperty as a subclass of cached_property with support for setters and deleters. I haven't tried it yet though.

  8. embray commented on Jan 16, 2021

    @embray
    Member

    Another point about both lazyproperty and cached_property is they both require types that have a __dict__, which is a limitation I have in fact run up against in some cases. In one of my other projects I implemented something similar to this but a little more sophisticated in that it can have a custom caching mechanism. The default is to use __dict__ but you can subclass it to provide a different mechanism for storing/retrieving cached values. This makes it usable with arbitrary types that don't have a __dict__, or even using external caches allowing already computed values to be reused across runs if applicable.

  9. mhvk commented on Jan 16, 2021

    @mhvk
    Contributor

    Looking again, I think cached_property is really a bit of a different beast. As it does not have __set__ and __delete__, it is not a data descriptor: the moment it sets its own name in parent.__dict__, any further access will retrieve that information -- the descriptor is now inactive (until one deletes the attribute). It also means one can just set the attribute at will. Really it is more like an attribute with a default value, while in lazyproperty one always goes through the descriptor to retrieve/set/delete the attribute.

    So, my sense is that when 3.8 is our minimum it will be good to have a more detailed look but I'd guess that only in some places cached_property is a good replacement for lazyproperty. What would probably be good is to update the docstring for lazyproperty to refer to cached_property and describe the differences.

  10. embray commented on Jan 18, 2021

    @embray
    Member

    I'm not sure what you mean by "the descriptor is now inactive". It works pretty much the same way lazyproperty does in that it stores the cached value in each instance's __dict__. The descriptor still remains in the class __dict__ and lookups of that attribute on the instance still go through the descriptor. The only difference is the lack of setter and deleter support. The rest is almost identical (esp. since #11221) which is a good point in its favor.

  11. mhvk commented on Jan 18, 2021

    @mhvk
    Contributor

    @embray - if I understand https://docs.python.org/3/reference/datamodel.html#invoking-descriptors properly, for cached_property, the moment it has stored the value in the instance __dict__, lookup will directly use that value when you look up the attribute, not go through cached_property.__get__ again, because cached_property has only defined __get__, not __set__, and thus is not a data descriptor.

  12. embray commented on Jan 29, 2021

    @embray
    Member

    I see, so it is a non-data descriptor (which confused me since just property is a data-descriptor). It's confusing terminology because it still returns "data" sigh.

  13. mhvk commented on Jan 29, 2021

    @mhvk
    Contributor

    Yes, I think it is quite confusing to call it cached_property (and I certainly was confused when I looked at the code first), as its setting and deleting have very different behaviour from that of a property. It is more like "an attribute with lazily evaluated default".

  14. pllim commented on Sep 3, 2021

    @pllim
    MemberAuthor

    We dropped Python 3.7 in #11934, so I think we can implement this now if we want.

  15. mhvk commented on Sep 3, 2021

    @mhvk
    Contributor

    Indeed, though as discussed above, we have to be really careful, since the behaviour is rather different. Definitely not a search-and-replace!

  16. pllim commented on Sep 24, 2021

    @pllim
    MemberAuthor

    Re-reading the discussions about concerns of cached_property is not a true "data descriptor," perhaps this will not be resolved all in one PR but rather have to be done in parts, one PR per sub-package, so the sub-package maintainers could each evaluate whether substitution is feasible or not. But it is looking like we can never truly get rid of our lazyproperty here.

    Currently, I see it used in the following core subpackages. I am sure it is also used downstream but I don't know where.

    • constants
    • coordinates
    • cosmology
    • io.fits
    • nddata
    • time
    • units
    • wcs

    I think once we have decided on which ones can be replaced and which cannot, we should open smaller issues that is actually actionable and close this one out.

  17. neutrinoceros commented on Sep 23, 2026

    @neutrinoceros
    Contributor

    This came up over this year's Coordination Meeting within the free-threading/multithreading breakout session. @larrybradley mentioned that @lazyproperty wasn't multi-threading friendly, so finishing this issue is a sub-goal towards free-threading compat.

  18. larrybradley commented on Sep 23, 2026

    @larrybradley
    Member

    @lazyproperty holds a single threading.RLock per property definition, shared by every instance of the class. In a free-threaded interpreter where many threads each work on their own object, those threads queue on the same lock and the expensive computation runs one at a time, defeating the parallelism.

    functools.cached_property removed a similar lock in Python 3.12 (python/cpython#101890) to allow parallel computations. I'm in favor of migrating to cached_property.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions