How does @functools.cached_property differ from @property on repeated attribute access?
answer
- One caches per instance, one does not
- First read writes into the instance
- Instance attribute shadows the class attribute afterwards
- Delete the attribute to force recomputation
- No instance dict means TypeError under __slots__
basics
~20 sproperty runs its function on every attribute read. functools.cached_property runs it on the first read only, stores the result in that instance's dict under the same name, and every later read finds that ordinary instance attribute, so the function is never called again.
solid answer
~50 s`property` recomputes on each access; `functools.cached_property` computes once per instance and then gets out of the way. On the first read it calls the function and writes the result into the instance's `__dict__` under the attribute's own name; because an instance attribute is found before this kind of class attribute, later reads never reach the decorator at all. That gives three consequences: the value never refreshes on its own, and you invalidate it by deleting the attribute (`del obj.total`) so the next read recomputes; assignment is allowed and simply overwrites the cached entry, unlike a `property` with no setter which raises `AttributeError`; and the whole thing needs a writable instance `__dict__`, so a class using `__slots__` without `'__dict__'` raises `TypeError` on first access. Since 3.12 there is no lock around the computation, so concurrent first accesses can run the function more than once, last write winning.
code
python · 16 linesimport functools
class Invoice:
def __init__(self, lines):
self.lines = lines
@functools.cached_property
def total(self):
print("summing")
return sum(self.lines)
inv = Invoice([10, 20])
print(inv.total, inv.total)
print(inv.__dict__)
del inv.total
print(inv.total)go deeper
Recall the one-line difference: property runs every time, cached_property runs once per instance and remembers. Knowing that the remembered value lives on the instance is the part being checked.
Explain why later accesses skip the decorator entirely — the stored instance attribute is found first — and what follows from it: manual invalidation by deleting the attribute, assignment being permitted, and the slots TypeError.
Demonstrate judgement about lifetime: the value lives exactly as long as the instance, invalidation is yours to design, and since 3.12 concurrent first accesses can run the body twice, so a body with side effects is a race you own.
Own the guidance on when a per-instance cache is the right lifetime at all versus a shared cache or no cache, and on the cost of code where invalidation is scattered across mutators. Name the slots pairing as a design constraint on hot, numerous objects.
## Two decorators, two lifetimes `property` turns a method into an attribute-shaped read: every `obj.total` calls the function. That is the right thing when the value tracks mutable state, and the wrong thing when the value is expensive and settled. `functools.cached_property`, added in 3.8, is the settled case: compute once per instance, then behave like a plain attribute. ```python import functools class Invoice: def __init__(self, lines): self.lines = lines @functools.cached_property def total(self): print("summing") return sum(self.lines) inv = Invoice([10, 20]) inv.total # prints 'summing', returns 30 inv.total # prints nothing, returns 30 inv.__dict__ # {'lines': [10, 20], 'total': 30} ``` ## The mechanism, in one sentence The first access calls your function and writes its result into `inv.__dict__['total']`. Attribute lookup consults the instance dictionary before it consults a class attribute of this kind, so from the second access onward the lookup finds the stored value and stops — the decorator is not consulted, adds no per-access cost, and could not intervene if it wanted to. That single fact explains everything else about it. ## Invalidation is manual There is no dependency tracking, no TTL, and no notification when `self.lines` changes. If the inputs move, the cached number is now a lie. You invalidate by removing the instance attribute: ```python del inv.total # or inv.__dict__.pop('total', None) inv.total # recomputes ``` `del` raises `AttributeError` if the value was never computed, which is why the `pop` form with a default is common in a `reset()` method. If you find yourself invalidating on every mutation, the honest answer is that you wanted a plain `property` — or a method — and the caching was premature. ## Assignment behaves differently from property A `property` with only a getter rejects assignment with `AttributeError`. A `cached_property` accepts it: `inv.total = 999` writes straight into the instance dictionary and every later read returns 999 without ever calling your function. That is occasionally useful for injecting a value in a test, and occasionally a bug when a caller assigns to what they assumed was read-only. If read-only matters, `property` is the correct tool. ## The __slots__ clash The value has to live in the instance dictionary — so the instance must have one. A class that declares `__slots__` and does not list `'__dict__'` has no instance dictionary, and the first access raises `TypeError`, complaining that there is no `__dict__` on the instance in which to cache the attribute: ```python import functools class Row: __slots__ = ("cells",) def __init__(self, cells): self.cells = cells @functools.cached_property def width(self): return len(self.cells) Row("abc").width # TypeError ``` The available moves are: add `'__dict__'` to `__slots__`, which restores the cache but gives back most of the memory saving `__slots__` bought; or cache by hand into a dedicated slot with an explicit sentinel; or drop the caching. It is worth knowing this pairing on sight, because `__slots__` and `cached_property` are both reached for in the same kind of code — many small objects, each with a derived value — and they do not compose. ## Naming and threads The decorator learns the attribute name when the class body is executed. Assigning the same `cached_property` object to a second class attribute, or attaching one to a class after the fact, leaves it without a usable name and raises `TypeError` on access; one decorator instance belongs to one attribute on one class. Until Python 3.11 the implementation held a lock so that concurrent first accesses computed the value once. Python 3.12 removed that lock, because it was shared across all instances of the class and serialised unrelated objects — a real bottleneck under threads. As of 3.12, and so on 3.14, several threads racing on the first access can each run the function; the results are all written to the same key and the last write wins. For a pure computation that is harmless duplicated work, but a `cached_property` whose body opens a connection, allocates a handle, or increments a counter is now a race you own. ## Choosing between them Use `property` when the value derives from state that changes, when the computation is cheap, or when read-only enforcement matters. Use `cached_property` when the instance is effectively immutable for the value's purposes, the computation is expensive enough to be worth remembering, and the instance's own lifetime is the right lifetime for the cached value — because that is exactly how long it will live.
- How do you invalidate a cached_property when the data it derives from changes?Delete the instance attribute — `del obj.total`, or `obj.__dict__.pop('total', None)` when it may never have been computed — and the next read recomputes. Nothing invalidates automatically; there is no TTL and no dependency tracking. If invalidation has to happen on every mutation, a plain `property` or an explicit method is the more honest design.
- What happens if a caller assigns directly to a cached_property attribute?The assignment lands in the instance `__dict__` and later reads return it; the decorated function never runs. That differs from a getter-only `property`, which raises `AttributeError` on assignment. It is handy for substituting a value in a test and dangerous when callers assume the attribute is read-only — if enforcement matters, use `property`.
- Why can a class using __slots__ not use cached_property, and what are the options?The result is cached by writing into the instance `__dict__`, and a slots class without `'__dict__'` in `__slots__` has none, so the first access raises `TypeError`. You can add `'__dict__'` to `__slots__` and lose most of the memory saving, cache by hand into a dedicated slot with a sentinel, or accept recomputation with a plain `property`.
A property is a question you ask the object every time; a cached_property is a question asked once and the answer written on the object in permanent marker — and rubbing it out is your job, not the object's.
saying these in an interview costs you the question
- Thinking a cached_property recomputes when its inputs change
- Expecting assignment to it to raise AttributeError like a property
- Believing the cached value is shared across all instances
- Assuming it works on any class regardless of __slots__
- Claiming the first access is locked so the body runs exactly once
- Saying the decorator still runs on every access to check the cache