How does functools.cached_property make every read after the first skip the descriptor?
answer
- Where does the cached value live?
- Which wins: instance dict or this descriptor?
- It omits __set__ and __delete__ on purpose
- Non-data descriptor, shadowed after first read
- __set_name__ captures the cache key
basics
~20 sIt is a non-data descriptor: the first read runs the function and stores the result in the instance __dict__ under the same attribute name. Later reads find that entry first, so the descriptor never runs again.
solid answer
~40 s`functools.cached_property` defines only `__get__` and `__set_name__` — no `__set__`, no `__delete__` — which makes it a **non-data descriptor**. Attribute lookup on an instance checks data descriptors on the class first, then the instance `__dict__`, then non-data descriptors. On the first read the instance dict has no entry, so lookup falls through to the descriptor, which calls the wrapped function and writes the result into `instance.__dict__[name]` — the same name the descriptor is bound to, captured by `__set_name__` at class creation. Every later read finds that dict entry in the earlier step and returns it directly, so there is no call, no lookup on the class, and no per-read overhead. That is the whole difference from `property`, which is a data descriptor and therefore runs its getter on every single access.
code
pycon · 17 lines>>> from functools import cached_property
>>> class Archive:
... def __init__(self, messages):
... self.messages = messages
... @cached_property
... def total_bytes(self):
... print("computing")
... return sum(len(m) for m in self.messages)
...
>>> a = Archive(["hi", "there"])
>>> a.total_bytes
computing
7
>>> a.total_bytes
7
>>> a.__dict__
{'messages': ['hi', 'there'], 'total_bytes': 7}go deeper
Be ready to say what it does in one line: the first read computes and remembers the value on that object, later reads reuse it. Know that it is per instance, not shared across objects.
Explain the mechanism, not just the effect: only __get__ is defined, so it is a non-data descriptor that loses to the instance __dict__, and it stores its result there under the name __set_name__ captured.
Show that you treat it as a memory and staleness decision. Say when the derived value is safe to freeze for the object's lifetime, and what holding it pins in memory on long-lived objects.
Own the guidance: where in a codebase lazy caching is a legitimate pattern versus where it hides an expensive call that should be explicit, and how you keep teams from caching values whose inputs mutate.
## What the decorator actually builds `@cached_property` replaces the decorated function with an instance of the `functools.cached_property` class stored as a **class attribute**. That object holds the original function and, once the class body finishes, the attribute name it was bound to — Python calls `__set_name__(owner, name)` on every class-body value at class-creation time, and `cached_property` uses that hook to learn the name it must cache under. This matters: if you attach one after the fact (`C.x = cached_property(f)`), `__set_name__` never fires and the first read raises `TypeError: Cannot use cached_property instance without calling __set_name__ on it.` Binding the same object to two names in one class body is also rejected, because one descriptor can only own one cache key. ## Why the second read is free Instance attribute lookup goes through `__getattribute__`, which resolves in a fixed order: 1. Walk the type's MRO for the name. If what it finds is a **data descriptor** — an object defining `__set__` or `__delete__` — call its `__get__` and stop. 2. Otherwise look in the instance `__dict__`. If the name is there, return that value and stop. 3. Otherwise fall back to the class attribute found in step 1; if it is a **non-data descriptor**, call its `__get__`. `cached_property` deliberately implements only `__get__` (plus `__set_name__`), so it lands in step 3 and loses to the instance dict. First access: the dict is empty for that name, step 2 misses, step 3 calls `__get__`, which computes `func(instance)` and assigns it into `instance.__dict__`. Second access: step 2 hits, and the descriptor is not consulted at all. The cached read costs one ordinary dict lookup — the same as reading `self.messages` — with no function call and no descriptor machinery. ```python from functools import cached_property class Archive: def __init__(self, messages): self.messages = messages @cached_property def total_bytes(self): print("computing") return sum(len(m) for m in self.messages) a = Archive(["hi", "there"]) a.total_bytes # prints "computing", returns 7 a.total_bytes # returns 7, prints nothing a.__dict__ # {'messages': [...], 'total_bytes': 7} ``` Note that `Archive.total_bytes` — access on the class, with no instance — returns the descriptor object itself, because `__get__` short-circuits when `instance is None`. That is how the decorator stays introspectable and how `help()` still sees the docstring, which `cached_property` copies from the wrapped function. ## The contract you take on **The cache is per instance and lives exactly as long as the instance.** There is no size bound, no expiry and no key: one value, stored beside the object's ordinary attributes. Two consequences follow. First, **staleness is yours to manage.** Nothing watches the inputs. If `total_bytes` is derived from a list you keep appending to, the cached number silently stops matching reality. The rule of thumb is to reach for `cached_property` only when the derived value is a function of state that does not change for the object's lifetime — a parsed header, a compiled pattern, a total over an immutable payload. When the inputs do change, you must invalidate explicitly. Second, **the cached value is retained as long as the instance is**, along with everything it references. Caching a large derived structure on a long-lived object is a memory decision, not just a speed one: profiling a service that holds thousands of such objects will show the derived data, not the source data, as the growth. ## Where it sits next to the alternatives `property` recomputes on every read and can validate on write; it is the right choice when the value must track changing state. Computing eagerly in `__init__` is simpler and cheaper to reason about when the value is always needed and cheap; `cached_property` earns its place when the computation is expensive *and* often not needed at all, so paying for it lazily on the objects that ask is the win. And because the descriptor loses to the instance dict, a plain assignment `obj.attr = value` before the first read simply pre-seeds the cache — which is convenient for tests and for injecting a precomputed value, and impossible with a read-only `property`. The one thing to say out loud in an interview is the mechanism, not the effect: *it is a non-data descriptor that writes into the instance dictionary under its own name, so subsequent lookups shadow it.* Every other property of `cached_property` — the `__slots__` restriction, `del` as the invalidation gesture, the thread-safety caveat — falls straight out of that sentence.
- What happens if you assign to the attribute before ever reading it?The assignment goes straight into the instance `__dict__`, because a non-data descriptor does not intercept writes. The wrapped function then never runs for that instance — the cache is simply pre-seeded. That is a real convenience for tests and for injecting a precomputed value, and it is impossible with a read-only `property`, which raises `AttributeError` on assignment.
- Why does accessing the attribute on the class rather than an instance not compute anything?`__get__` receives `instance=None` for a class-level access and returns the descriptor object itself. There is no instance dictionary to cache into and no `self` to pass to the wrapped function, so returning the descriptor is the only sensible answer — and it keeps the attribute introspectable, which is why `help()` and documentation tools still see the original docstring.
- When would you prefer computing the value eagerly in __init__ instead?When the value is always needed, or cheap, or when construction is the natural place to fail. Eager computation makes cost and errors visible at a predictable point and keeps the object's state obvious. `cached_property` pays off when the computation is expensive *and* frequently skipped, so you only spend it on the objects that actually ask for the value.
It is a signpost that, the first time someone follows it, gets replaced by the destination itself standing in the road — nobody after that reads the sign.
saying these in an interview costs you the question
- Calling it a data descriptor
- Saying the value is cached on the class, shared by all instances
- Claiming it recomputes when the underlying attributes change
- Confusing it with a per-arguments function cache keyed on inputs
- Believing the descriptor runs on every attribute read
- Assuming the cache is bounded or expires on its own