Python cached_property: Cache Object Values

Published on: July 25, 2026
Reading time: 6 minutes
Python code using cached_property to store expensive calculations

Some Python objects contain values that are cheap to store and others that require file reads, collection processing, service calls, or repeated calculations. When a result depends only on the object’s state and can be reused, recomputing it on every access wastes time. Python cached_property addresses this situation by turning a method into an attribute that is calculated once and stored on the instance.

This guide explains how to use functools.cached_property, invalidate stored values, prevent stale results, handle concurrency, test the behavior, and choose between cached_property, property, and lru_cache. The feature builds naturally on object-oriented Python, Python functions, descriptors, and dataclasses.

What is cached_property?

cached_property is a standard-library descriptor available from functools. On the first access, it runs the decorated method, stores the result in the instance dictionary, and returns it. Later accesses find the stored attribute directly, so the method is not called again.

This is different from a normal property. A property getter runs whenever the attribute is read. A cached property trades memory for computation: each instance keeps its own result until the object is discarded or the attribute is removed.

The official cached_property documentation explains instance storage, __dict__ requirements, and concurrent access. The official descriptor guide provides the background needed to understand why the descriptor controls the first lookup and then allows the stored instance attribute to take over.

A first example

Consider a class representing a report loaded from a file. Reading and transforming the data may be expensive, but the parsed content will probably be used several times:

from functools import cached_property
from pathlib import Path
import json

class Report:
    def __init__(self, path: str):
        self.path = Path(path)

    @cached_property
    def data(self) -> list[dict]:
        print("Reading and processing the file...")
        text = self.path.read_text(encoding="utf-8")
        return json.loads(text)

report = Report("sales.json")

print(len(report.data))
print(report.data[0])

The message appears only during the first access. After that, data is present in the instance __dict__:

print(report.__dict__)
# {'path': Path('sales.json'), 'data': [...]}

The cache belongs to the instance, not the whole class. Two Report objects calculate and store independent values.

Invalidating the cached value

Delete the attribute to clear the stored result. The next access calls the method again:

del report.data
print(report.data)

This behavior makes explicit invalidation possible. When another attribute changes and makes the old result incorrect, remove the cached entry.

class Report:
    def __init__(self, path: str):
        self._path = Path(path)

    @property
    def path(self) -> Path:
        return self._path

    @path.setter
    def path(self, new_path: str) -> None:
        self._path = Path(new_path)
        self.__dict__.pop("data", None)

    @cached_property
    def data(self) -> list[dict]:
        text = self._path.read_text(encoding="utf-8")
        return json.loads(text)

Using pop avoids an exception when the value has never been calculated. The key must exactly match the decorated method name.

The main risk: stale cache entries

cached_property does not know which fields influence a calculation. When object state changes, the decorator does not recalculate automatically. It works best for immutable objects, data loaded once, or results that remain valid for the entire lifetime of an instance.

Consider an order class:

from functools import cached_property

class Order:
    def __init__(self, items):
        self.items = items

    @cached_property
    def total(self):
        return sum(
            item["price"] * item["quantity"]
            for item in self.items
        )

If code modifies order.items after the first access to total, the cached amount becomes wrong. The main options are to make the source data immutable, invalidate the cache at every mutation point, or avoid caching that property. The last choice is usually safer when changes are frequent or difficult to control.

Mutable return values need care

Every access returns the same stored object. If the property returns a list or dictionary and a caller mutates it, the modification becomes part of the cache:

data = report.data
data.clear()

print(report.data)

When callers should not modify the result, return a tuple, frozenset, immutable data structure, or controlled copy. Another option is to keep a private cached property and expose a method that returns a copy.

Using cached_property with dataclasses

The decorator works naturally with ordinary dataclasses as long as the instance has a __dict__:

from dataclasses import dataclass
from functools import cached_property
from statistics import mean

@dataclass
class Course:
    name: str
    grades: tuple[float, ...]

    @cached_property
    def average(self) -> float:
        if not self.grades:
            return 0.0
        return mean(self.grades)

Because the grades are stored in a tuple, the calculated average remains consistent. A frozen dataclass may still provide the internal dictionary used by the descriptor, but test the exact structure used by the project and do not assume that every immutability mechanism behaves identically.

Limitations with __slots__

cached_property needs a mutable __dict__ in which to save the result. A class using __slots__ without a dictionary does not provide that storage:

class Point:
    __slots__ = ("x", "y")

    def __init__(self, x, y):
        self.x = x
        self.y = y

In this situation, use a normal property, reserve a dedicated slot for the computed value, or implement an external cache. Adding "__dict__" to __slots__ restores compatibility, but it also reduces some of the memory savings that motivated slots.

Concurrency and duplicate execution

Do not treat cached_property as a guarantee that a method runs exactly once across multiple threads. Two threads can start the calculation before either one stores the result. This is usually acceptable when the method is idempotent and has no side effects.

If duplicate execution could trigger billing, writes, state changes, or another harmful effect, protect the critical operation with an instance lock:

from functools import cached_property
from threading import Lock

class Client:
    def __init__(self, api):
        self.api = api
        self._profile_lock = Lock()

    @cached_property
    def profile(self):
        with self._profile_lock:
            return self.api.fetch_profile()

This prevents overlapping calls through the same instance, although highly concurrent applications may need a more complete strategy with timeout, error handling, and explicit invalidation.

What happens when calculation fails?

If the method raises an exception, no successful value is stored. A later access tries the method again. This is convenient for transient failures, but it can also repeat an expensive operation indefinitely.

@cached_property
def configuration(self):
    if not self.path.exists():
        raise FileNotFoundError(self.path)
    return load_configuration(self.path)

Decide whether retrying on every access is appropriate. For remote integrations, controlled retries, logging, or a separate state object may be better. Do not convert exceptions into silent fallback values merely to fill the cache.

cached_property or property?

Choose property when the value must always reflect current state, the computation is inexpensive, or validation should run on every read. Choose cached_property when the calculation has meaningful cost, the result stays stable, and the instance can store it.

class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    def diameter(self):
        return self.radius * 2

Caching such a small calculation provides little benefit. The additional memory and invalidation rules would be more expensive than the multiplication.

cached_property or lru_cache?

lru_cache stores results for combinations of function arguments. It is well suited to pure functions called repeatedly. When applied to methods, it can include self in the cache key, which requires a hashable instance and can keep objects alive while entries remain cached.

cached_property is simpler when there is exactly one argument-free value per instance. The result follows the lifetime of the object and can be invalidated with del. In contrast, lru_cache provides entry limits, hit and miss statistics, and function-wide clearing.

Testing cached behavior

A useful test verifies the result, the number of method executions, and invalidation:

from functools import cached_property

class Example:
    def __init__(self):
        self.calls = 0

    @cached_property
    def value(self):
        self.calls += 1
        return 42

def test_cached_property():
    obj = Example()

    assert obj.value == 42
    assert obj.value == 42
    assert obj.calls == 1

    del obj.value

    assert obj.value == 42
    assert obj.calls == 2

For mutable objects, also test that every relevant setter removes the correct key from __dict__.

Memory considerations

Each computed property adds a value to the instance dictionary. A few entries are usually negligible, but thousands of long-lived objects with large cached results can consume significant memory. The decorator can also affect key-sharing dictionaries used by CPython for memory efficiency.

Measure before optimizing. If objects are numerous, results are large, or only a small percentage of instances use the property, an external bounded cache or explicit lazy-loading component may provide better control.

Best practices

  • Cache only calculations with meaningful cost.
  • Prefer results that remain stable for the instance lifetime.
  • Document which mutations require invalidation.
  • Avoid side effects inside the cached method.
  • Protect non-idempotent work in concurrent scenarios.
  • Be careful when returning mutable collections.
  • Test first access, reuse, failure, and deletion.
  • Confirm __dict__ support when using slots or special types.

Conclusion

Python cached_property is a small but valuable tool for objects with expensive and stable derived values. The first access calculates the result; later accesses treat it like an ordinary instance attribute. The API remains clean while repeated processing is avoided.

The optimization is only reliable when the validity rules are clear. Before applying the decorator, identify the source fields, whether they can change, how the value will be invalidated, and whether concurrent accesses are possible. When those answers are simple, cached_property is readable and effective. When they are not, a normal property or an explicit caching layer is usually safer.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Python code with type-based function dispatch
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python singledispatch: Type-Based Functions

    Learn Python singledispatch to build type-based functions, reduce isinstance chains, and organize extensible polymorphism with clear examples.

    Ler mais

    Tempo de leitura: 6 minutos
    25/07/2026
    Python code demonstrating descriptors and attributes
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python Descriptors: Practical Guide

    Learn Python descriptors with __get__, __set__, validation, property, instance storage, testing, inheritance and practical design tips.

    Ler mais

    Tempo de leitura: 6 minutos
    22/07/2026
    Caixas empilhadas
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python Modules and Packages: Complete Guide

    Learn how to create Python modules and packages, organize imports, use __init__.py, run modules, avoid circular imports, and structure real

    Ler mais

    Tempo de leitura: 5 minutos
    10/07/2026
    logo do python com objetos abaixo do logo
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Object-Oriented Python: Classes and Objects

    Learn object-oriented Python with classes, objects, attributes, methods, constructors, inheritance, composition, encapsulation, properties, and practical examples.

    Ler mais

    Tempo de leitura: 6 minutos
    10/07/2026
    Programação assíncrona com asyncio em Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python asyncio: A Practical Beginner’s Guide

    Learn Python asyncio with coroutines, await, tasks, TaskGroup, timeouts, cancellation, queues, locks, error handling, and blocking work.

    Ler mais

    Tempo de leitura: 5 minutos
    10/07/2026
    Uso de expressões regulares regex para manipulação de texto em Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python Regex: Complete Regular Expressions Guide

    Learn Python regex with re.search, fullmatch, findall, groups, quantifiers, substitutions, flags, compiled patterns, and practical examples.

    Ler mais

    Tempo de leitura: 7 minutos
    10/07/2026