# Why does Django implement its own version of @cached\_property ?

**URL:** <https://forum.djangoproject.com/t/why-does-django-implement-its-own-version-of-cached-property/46029>\
**Category:** Django Internals\
**Created:** [September 16, 2026, 12:25pm UTC](https://forum.djangoproject.com/t/why-does-django-implement-its-own-version-of-cached-property/46029 "2026-09-16T12:25:11Z")\
**Posts on this page:** 4\
**Page:** 1

<div class="post-metadata">

**Author:** ![jrief](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/jrief/32/17040_2.png) [@jrief](https://forum.djangoproject.com/u/jrief)\
**Post date:** [September 16, 2026, 12:25pm UTC](https://forum.djangoproject.com/t/why-does-django-implement-its-own-version-of-cached-property/46029/1 "2026-09-16T12:25:11Z")

</div>

Python offers the method decorator `@` **`cached_property`** since version 3.9. The Django implementation seems to be older, but it nevertheless offers a similar functionality. Is there any specific reason for this? Otherwise, and for consistency, I would propose to just use the builtin version from Python itself.

---

<div class="post-metadata">

**Author:** ![jacobtylerwalls](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/jacobtylerwalls/32/30603_2.png) [@jacobtylerwalls](https://forum.djangoproject.com/u/jacobtylerwalls)\
**Post date:** [September 16, 2026, 12:30pm UTC](https://forum.djangoproject.com/t/why-does-django-implement-its-own-version-of-cached-property/46029/2 "2026-09-16T12:30:04Z")

</div>

There was a concern about performance and that it might possibly disappear from the standard library. We could take another look now, but the benefit seems slim?

> **[\#30949 (Use functools.cached\_property instead of...](https://code.djangoproject.com/ticket/30949)**

---

<div class="post-metadata">

**Author:** ![jrief](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/jrief/32/17040_2.png) [@jrief](https://forum.djangoproject.com/u/jrief)\
**Post date:** [September 16, 2026, 1:19pm UTC](https://forum.djangoproject.com/t/why-does-django-implement-its-own-version-of-cached-property/46029/3 "2026-09-16T13:19:00Z")

</div>

Now after 6 Python releases, this decorator won’t go away.

The benefit might be slim, but it’s confusing to have the same decorator twice, doing (almost?) the same thing.

How about replacing `django.utils.functional.cached_property` against `functools.cached_property` and check if all unit tests works as expected? And if so, we can start a discussion with a wider audience.

---

<div class="post-metadata">

**Author:** ![adamchainz](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/adamchainz/32/26_2.png) [@adamchainz](https://forum.djangoproject.com/u/adamchainz)\
**Post date:** [September 17, 2026, 7:47pm UTC](https://forum.djangoproject.com/t/why-does-django-implement-its-own-version-of-cached-property/46029/4 "2026-09-17T19:47:45Z")

</div>

> **Django's implementation**
>
> ```auto
> class cached_property:
> """
> Decorator that converts a method with a single self argument into a
> property cached on the instance.
> 
> A cached property can be made out of an existing method:
> (e.g. ``url = cached_property(get_absolute_url)``).
> """
> 
> name = None
> 
> @staticmethod
> def func(instance):
> raise TypeError(
> "Cannot use cached_property instance without calling __set_name__ () on it."
> )
> 
> def __init__ (self, func):
> self.real_func = func
> self. __doc__ = func. __doc__
> 
> def __set_name__ (self, owner, name):
> if self.name is None:
> self.name = name
> self.func = self.real_func
> elif name != self.name:
> raise TypeError(
> "Cannot assign the same cached_property to two different names "
> "(%r and %r)." % (self.name, name)
> )
> 
> def __get__ (self, instance, cls=None):
> """
> Call the function and put the return value in instance. __dict__ so that
> subsequent attribute access on the instance returns the cached value
> instead of calling cached_property. __get__ ().
> """
> if instance is None:
> return self
> res = instance. __dict__ [self.name] = self.func(instance)
> return res
> 
> ```

> **Python's implementation**
>
> from types import GenericAlias
> 
> \_NOT\_FOUND = object()
> 
> class cached\_property:  
> def **init** (self, func):  
> self.func = func  
> self.attrname = None  
> self. **doc** = func. **doc**  
> self. **module** = func. **module**
> 
> ```
> def __set_name__ (self, owner, name):
> if self.attrname is None:
> self.attrname = name
> elif name != self.attrname:
> raise TypeError(
> "Cannot assign the same cached_property to two different names "
> f"({self.attrname!r} and {name!r})."
> )
> 
> def __get__ (self, instance, owner=None):
> if instance is None:
> return self
> if self.attrname is None:
> raise TypeError(
> "Cannot use cached_property instance without calling __set_name__ on it."
> )
> try:
> cache = instance. __dict__
> except (
> AttributeError
> ): # not all objects have __dict__ (e.g. class defines slots)
> msg = (
> f"No ' __dict__' attribute on {type(instance). __name__!r} "
> f"instance to cache {self.attrname!r} property."
> )
> raise TypeError(msg) from None
> val = cache.get(self.attrname, _NOT_FOUND)
> if val is _NOT_FOUND:
> val = self.func(instance)
> try:
> cache[self.attrname] = val
> except TypeError:
> msg = (
> f"The ' __dict__' attribute on {type(instance). __name__!r} instance "
> f"does not support item assignment for caching {self.attrname!r} property."
> )
> raise TypeError(msg) from None
> return val
> 
> __class_getitem__ = classmethod(GenericAlias)
> 
> ```

Python’s implementation contains a few more guards around weird class edge cases, like no ` __dict__ ` or an unwriteable ` __dict__ `. It also provides a typing hook in ` __class_getitem__ `. Other than those things, it’s basically the same. It seems Python even copied the error message wording from Django.

I would say let’s migrate, which in practice means deprecating our version while moving all internal usage to Python’s version. Django won here and got one of its functions merged upstream (albeit with some drama around adding then removing locking), so let’s use it 🙂
