Using Python functools.cached_property
python functools cached_property: Learn how functools.cached_property caches instance attributes, when to use it, and how it compares to property and lru_cache.
python functools cached_property requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
When a Python class has an attribute that is expensive to compute, accessing it repeatedly can waste time and resources. The functools.cached_property decorator provides a way to compute the value once per instance and store it for subsequent accesses. This article explains how it works, when to use it, and how it differs from other caching mechanisms.
Consider a class that loads a large configuration file each time a property is accessed:
class Config: def __init__(self, path): self.path = path @property def data(self): with open(self.path) as f: return f.read()
Every access to config.data re-reads the file from disk. If the file does not change during the program's lifetime, that repeated I/O is unnecessary. Using functools.cached_property instead of property changes the behavior so the file is read only once per instance:
from functools import cached_property class Config: def __init__(self, path): self.path = path @cached_property def data(self): with open(self.path) as f: return f.read()
Now config.data reads the file on the first access and caches the resulting string. Subsequent accesses return the cached value without touching the file again.
The Problem: Recomputing Expensive Attributes
Many Python classes expose attributes that require computation, network calls, database queries, or file I/O. A plain @property recalculates the value every time it is accessed. If the underlying data is immutable and the calculation is costly, this repeated work adds unnecessary latency and load.
cached_property solves this by memoizing the result on the instance itself. The first access runs the decorated method and stores the return value in the instance's __dict__. Later accesses find the stored value and return it directly, skipping the method body entirely.
How cached_property Works
cached_property is a non-data descriptor. When you access an instance attribute with the same name, Python's attribute lookup order checks the instance dictionary first. Because cached_property does not define __set__, it is a non-data descriptor, so writes to the attribute override the descriptor.
On first access, the descriptor calls the wrapped method, then stores the result in instance.__dict__[attrname]. On subsequent accesses, Python finds the entry in the instance dictionary and returns it, bypassing the descriptor. This mechanism is simple and efficient.
One important consequence is that cached_property only works on classes that have a __dict__. If a class uses __slots__ without including '__dict__' in the slots list, cached_property will raise an AttributeError when it tries to write to the instance dictionary.
When to Use cached_property vs property
Use a plain property when the attribute is cheap to compute, or when the value may change during the lifetime of the instance. For example, a property that returns the current time or a counter should not be cached.
Use cached_property when the value is expensive to compute and is expected to remain constant for the life of the instance. Typical examples include:
- Parsing a configuration file
- Loading a large data set from disk
- Establishing a database connection
- Performing a complex calculation that depends only on immutable instance state
If the value depends on mutable state that can change, caching it can lead to stale results. In that case, either invalidate the cache explicitly or use a regular property.
cached_property vs functools.lru_cache
functools.lru_cache is another caching decorator, but it works at the function level and caches results based on the arguments passed to the function. It is often used on standalone functions or methods where the same arguments produce the same result.
| Feature | cached_property | lru_cache |
|---|---|---|
| Scope | Per instance | Per function call, keyed by arguments |
| Cache storage | Instance __dict__ | Function-level cache dictionary |
| Invalidation | Delete the instance attribute | Call cache_clear() or use cache_info() |
| Thread safety | Not guaranteed | Not guaranteed unless threading is used |
| Typical use | Expensive instance attributes | Expensive function calls with repeated arguments |
For a method that takes arguments, lru_cache can be useful, but it caches globally across all instances unless you are careful. cached_property is specifically designed for instance-level caching and is more idiomatic for attributes.
Thread Safety and Concurrency
cached_property does not provide any locking. If multiple threads access the same instance attribute concurrently for the first time, the decorated method may run more than once, and each thread may see a different result. In many cases this is acceptable because the value is deterministic and the extra computation is harmless. However, if the method has side effects or is very expensive, you should protect it with a lock.
import threading from functools import cached_property class ExpensiveResource: def __init__(self): self._lock = threading.Lock() @cached_property def resource(self): with self._lock: # Check if another thread already computed it if 'resource' in self.__dict__: return self.__dict__['resource'] # Perform expensive setup return create_resource()
This pattern is not built into cached_property, so you must implement it yourself if you need strict single-execution guarantees.
Invalidating the Cache
There are times when the underlying data changes and you need to force a recomputation. Because cached_property stores the value in the instance dictionary, you can delete it to invalidate the cache:
config = Config('settings.ini') print(config.data) # Reads file del config.data # Removes the cached value print(config.data) # Reads file again
This is a simple and effective way to refresh the attribute. You can also assign a new value directly, but that would bypass the descriptor and set the attribute to whatever you assign, which may not be what you want. Deleting the attribute is the standard invalidation mechanism.
Practical Example: Lazy Loading a Database Connection
A common use case is establishing a database connection only when it is first needed. cached_property makes this straightforward:
import sqlite3 from functools import cached_property class Database: def __init__(self, path): self.path = path @cached_property def connection(self): return sqlite3.connect(self.path)
Here, db.connection creates the connection on first access and reuses it for the rest of the object's life. This avoids the overhead of opening a new connection on every access and ensures that the connection is created only if the code actually uses it.
Performance and Memory Considerations
cached_property trades memory for computation. Each cached attribute stores an extra reference in the instance dictionary. For objects with many cached attributes, this can increase memory usage. If the cached value is large and the object is short-lived, the memory overhead may outweigh the performance benefit.
Also, be aware that cached_property holds a strong reference to the cached value. If the value references the instance itself, you can create a reference cycle that prevents garbage collection. This is rarely a problem in practice, but it is worth keeping in mind when caching large or interconnected objects.
In general, use cached_property when the computation is significantly more expensive than the memory required to store the result, and when the value is accessed multiple times. If the attribute is accessed only once, caching adds no benefit and only consumes memory.
Compatibility and Availability
functools.cached_property was introduced in Python 3.8. If you are working with an older version, you can implement a similar decorator manually or use the cached-property package from PyPI. The standard library implementation is efficient and well-tested, so upgrading to Python 3.8 or later is the recommended path.
When using cached_property, remember that it is a non-data descriptor. This means you can override the cached value by assigning to the attribute directly, but that assignment will replace the cached value with the new object. If you want to force a recomputation, delete the attribute instead.
cached_property is a valuable tool for optimizing Python classes that expose expensive, immutable attributes. By understanding its behavior, threading implications, and invalidation methods, you can use it effectively without introducing subtle bugs.