How does functools.cached_property differ from @property stacked on functools.lru_cache?
answer
- One stores per object, one stores per class
- The value lands in the instance's own attributes
- Delete the attribute to invalidate
- Keying on self pins the instance
- No maxsize, no cache_clear on the first one
basics
~20 sfunctools.cached_property computes once per instance and stores the value on that instance, so later reads are ordinary attribute lookups and it dies with the object. A property over functools.lru_cache shares one cache keyed by self, pinning every instance.
solid answer
~50 s`functools.cached_property` runs the getter the first time the attribute is read on an instance and stores the result in that instance's own attribute dictionary under the same name. Later reads never reach the getter at all - they find an ordinary attribute - so there is no per-read overhead, the value is per instance, and it is freed with the instance. Invalidation is `del obj.attr`, which raises `AttributeError` if the value was never computed; there is no `cache_clear()` and no `maxsize`. Stacking `@property` on top of `@functools.lru_cache` looks similar but behaves differently: there is one cache on the function, shared by every instance and keyed by `self`, so `self` must be hashable and the cache holds a strong reference that keeps each instance alive for as long as its entry lives. That is a classic slow leak in a long-running process.
code
python · 17 linesimport functools
class Report:
def __init__(self, rows):
self.rows = rows
self.builds = 0
@functools.cached_property
def summary(self):
self.builds += 1
return sum(self.rows)
r = Report([1, 2, 3])
print(r.summary, r.summary, r.builds) # 6 6 1
print("summary" in vars(r)) # True: stored on the instance
del r.summary # the only invalidation lever
print(r.summary, r.builds) # 6 2go deeper
Know that functools.cached_property makes an attribute compute on first read and stay computed for that object, and that reading it again does not run the method body.
Explain that the value is stored on the instance under the same name, that del on the attribute is the invalidation, and why putting lru_cache on a method keys the cache on self.
Show that you would spot the shared-cache-on-a-method shape in review as a slow leak, and that you treat cached_property as safe only on state that stops changing after construction.
Own the guidance: which derived values may be cached on objects at all, how staleness is prevented rather than detected, and when the computation belongs outside the object entirely.
## Two mechanisms that look alike Both spellings give you "compute this attribute once", and they differ in *where the value lives*, which decides lifetime, invalidation and thread behaviour. `functools.cached_property` is applied to a method taking only `self`. On the first read of the attribute, the getter runs and the result is written into the instance's own attribute dictionary under the attribute's name. Every later read finds that ordinary instance attribute first and never involves the caching machinery again. So the cost after the first read is zero beyond a normal attribute lookup, the value is scoped to one object, and it becomes garbage when the object does. `@property` stacked over `@functools.lru_cache` puts the cache on the *function*, which is a single object owned by the class. The key is the only argument the getter receives, `self`. One table therefore serves every instance ever passed to it. ## What follows from the storage location **Lifetime.** With `cached_property`, dropping the instance drops the value. With the shared cache, the entry keys on the instance, and dictionary keys are strong references, so the instance cannot be collected while its entry is in the table. With `maxsize=None` no entry is ever evicted, so every instance the property was read on stays alive for the life of the process - memory that grows with the number of objects created, not with the number of distinct results. **Hashability.** The shared-cache form requires `self` to be hashable and to have equality that means what you want. A class that defines `__eq__` without `__hash__` becomes unhashable and the property raises `TypeError`; a class with value equality makes two distinct-but-equal instances share one cached result. **Invalidation.** `cached_property` has exactly one lever: delete the attribute, `del obj.summary`, which raises `AttributeError` if it was never computed, so a defensive invalidator uses `obj.__dict__.pop("summary", None)` or catches the error. You can also assign to the attribute to override the cached value, because it is a plain instance attribute. The shared-cache form has `cache_clear()`, which clears the value for *every* instance at once - rarely what you meant. **Storage requirements.** `cached_property` needs somewhere to write, so a class using `__slots__` and no instance dictionary raises `TypeError` on first read. It also cannot be used with a class attribute name it is not bound to, since it writes under the name it was assigned. **Threads.** Until 3.12, `cached_property` took a lock while computing, and that lock was shared class-wide, which serialized unrelated instances and was a real bottleneck. 3.12 removed it. On 3.14 there is no lock: two threads reading the attribute on a fresh instance can both run the getter, and one result simply overwrites the other. For a pure computation that is harmless; for a getter with side effects, or one that must yield a single shared object every reader compares by identity, you need your own lock. ## Choosing between them For a value derived from one object's own state, `cached_property` is the right tool: per-instance lifetime, no key, no eviction bookkeeping, no leak. It is at its best for a derived summary of immutable-ish state - a parsed form of a field, a total over a list that does not change after construction. The shared-cache form is worth reaching for only when the result genuinely is shared across instances and the key space is small and long-lived - a lookup keyed by a small value, not by `self`. If you find yourself writing `@property` over `@functools.lru_cache`, the usual better shape is to move the cache to a module-level function keyed on the small identifying values, and have the property call it. The cache then keys on a string or an integer instead of an object graph, and nothing is pinned in memory. A last point interviewers listen for: `cached_property` makes staleness silent. If the underlying attributes change after the first read, the cached value keeps its old answer with no warning. It suits objects that are effectively immutable after construction, and it is a bug waiting to happen on a long-lived mutable object.
- How do you invalidate a functools.cached_property, and what goes wrong if it was never read?Delete the attribute with del obj.attr, which removes the stored value from the instance so the next read recomputes. If the value was never computed there is nothing to delete and the statement raises AttributeError, so a general-purpose invalidator either catches that or pops the name from the instance dictionary with a default. There is no cache_clear() and no maxsize on this decorator.
- Why does functools.cached_property fail on a class that defines __slots__?It works by writing the computed value into the instance's attribute dictionary, and a class using __slots__ without also declaring a dictionary has none, so the first read raises TypeError complaining it has nowhere to cache. If you want both the memory savings of slots and a lazily computed value, compute it eagerly in the initializer into a declared slot, or keep a sentinel slot and check it yourself.
- Is functools.cached_property thread-safe?Not in the sense of computing exactly once. Versions before 3.12 held a class-wide lock, which serialized unrelated instances and was removed in 3.12, so on 3.14 two threads reading the attribute on a fresh instance can both run the getter and one result overwrites the other. That is fine for a pure computation and wrong when the getter has side effects or when every reader must see the same object by identity - add your own lock in those cases.
cached_property is a note stuck inside one object's own folder, thrown out with the folder. A property over a shared cache is a central ledger that keeps a copy of every folder it ever saw, so nothing can be thrown out.
saying these in an interview costs you the question
- Thinks cached_property shares one value across instances
- Calls cache_clear() on a cached_property
- Puts lru_cache on a method without noticing self is the key
- Assumes the cached value refreshes when the object mutates
- Believes the getter still runs on every attribute read