Skip to content

← Stdlib Mastery step 25 of 55

Medium Primitives

cached_property: compute once, release with the instance, invalidate on demand

Build a class whose expensive derived value is computed at most once per instance, released when the instance dies, and explicitly invalidatable.

class Report:
    def __init__(self, rows: list[int]) -> None: ...
    rows: list[int]
    computations: int
    total: int              # the derived value: sum(self.rows)
    def invalidate(self) -> None: ...
  • total is sum(self.rows). Reading it repeatedly must increment computations once.
  • invalidate() discards the cached value so the next read recomputes. Calling it on an instance that has never computed total must not raise.
  • Report.__init__ should copy the incoming rows (list(rows)).
def solve(rows: list[int], script: list[str]) -> dict[str, object]:

solve builds a Report(rows) and replays a script of operations:

op effect
"read" append report.total to the reads list
"invalidate" call report.invalidate()
"add" report.rows.append(5)

Anything else raises ValueError. Afterwards it records computations, takes a weakref.ref to the report, dels it, calls gc.collect(), and returns {"reads": [...], "computations": int, "leaked": bool} where "leaked" is tracker() is not None and must be False.

Why the leak test is here. The obvious hand-rolled alternative is a class-level dict keyed on the instance. It caches correctly, it invalidates correctly, and it keeps every Report alive for the life of the process. cached_property stores the value in the instance’s own __dict__, so the cache is part of the instance and dies with it.

Two things about cached_property you should know while writing this.

The per-property lock was removed in 3.12 — it serialised every thread touching any instance of the class, for a guarantee most callers did not need. Concurrent computation is now possible and last-writer-wins. Code written before 3.12 that relied on the implicit once-only guarantee under threading is now racy, with no deprecation warning.

A class with __slots__ and a cached_property type-checks clean under --strict and raises TypeError: No '__dict__' attribute ... to cache on first access. It is the best one-line demonstration in the course that --strict is necessary and not sufficient.

There is no invalidate() in the stdlib. del obj.total works but raises AttributeError when nothing was computed, so a general invalidator needs the pop form — reaching into __dict__ here is the supported mechanism, not a violation, because that is the documented storage location.