lru_cache in depth: methods, keys and memory

functools · Advanced · 11 min

Misused caches leak memory, return stale or shared mutable data, and miss more often than expected.

Choosing maxsize: the default is 128. An unbounded cache (maxsize=None or @cache) is fastest but grows forever, which is a leak in long-running services with unbounded argument spaces. Check cache_info() in production to see hit rates and sizes.

Cache keys are built from positional and keyword arguments as passed. Equal values of different types, like 2 and 2.0, usually share an entry (they compare equal and hash alike) unless typed=True; the docs say "usually" because a single int or str argument takes a fast path with its own key. f(a=1, b=2) and f(b=2, a=1) are different keys.

Methods: decorating a method with @lru_cache caches on the function object at class level, with self as part of the key. The cache holds a strong reference to every instance it has seen, so instances are never garbage-collected while their entries remain (linters flag this as B019). Options: cached_property for zero-argument methods, a per-instance cache created in __init__ (self.lookup = lru_cache()(self._lookup)), or a module-level function keyed by an ID.

Unhashable arguments: convert lists to tuples and dicts to frozenset(d.items()) in a thin wrapper before calling the cached function.

Time-based expiry isn't built in. Include a coarse time bucket in the key (int(time.time() // 300)), or use a library such as cachetools for TTL caches.

Example

import gc
import weakref
from functools import lru_cache

@lru_cache(maxsize=None)
def scale(x, factor):
    return x * factor
scale(2, 3); scale(2.0, 3)
print(scale.cache_info().currsize)       # 1  -- 2 == 2.0 share a key
typed = lru_cache(maxsize=None, typed=True)(scale.__wrapped__)
typed(2, 3); typed(2.0, 3)
print(typed.cache_info().currsize)       # 2

class Repo:
    @lru_cache(maxsize=None)              # caches on the class, keyed by self
    def lookup(self, key):
        return key.upper()

r = Repo(); r.lookup("a")
alive = weakref.ref(r)
del r; gc.collect()
print(alive() is not None)               # True: the cache keeps r alive
Repo.lookup.cache_clear(); gc.collect()
print(alive() is None)                   # True

Remember

  • Bound the cache in long-running processes; watch cache_info()
  • typed=True separates 1 and 1.0; keyword order matters for keys
  • @lru_cache on methods keeps instances alive; prefer per-instance caches

Watch out for

  • Decorating methods of short-lived objects with @lru_cache, causing a memory leak.

Check yourself

  1. Why can @lru_cache on an instance method leak memory?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/functools.html#functools.lru_cache