Quick summary
Summarize this blog with AI
functools.lru_cache remembers function results in the current Python process. Use it when the same hashable inputs should produce the same reusable result and recomputation is expensive. This lesson shows eviction, cache statistics, invalidation, and the limits that matter in a real program.
The examples use only the standard library and were tested with Python 3.12.14. Run the blocks in order in a fresh interpreter.
Observe a bounded LRU cache
This deliberately small cache stores two argument/result pairs. The list records actual function executions so that a cache hit is visible without a timing benchmark.
from functools import lru_cache
executed = []
@lru_cache(maxsize=2)
def square(number):
executed.append(number) # Instrumentation for this demonstration only.
return number * number
print([square(n) for n in (2, 3, 2, 4, 3)])
print(executed)
print(square.cache_info())
Expected output:
[4, 9, 4, 16, 9]
[2, 3, 4, 3]
CacheInfo(hits=1, misses=4, maxsize=2, currsize=2)
The second call with 2 hits the cache and makes that entry most recently used. Adding 4 then evicts 3, so the final call with 3 recomputes its result. A hit avoids the function body entirely; do not put required logging, writes, or other business side effects inside a cached function.
Measure reuse before increasing the size
cache_info() reports hits, misses, the entry limit, and the current entry count. Compare the avoided work with the memory occupied by real arguments and results. A size of 128 limits entries, not bytes. maxsize=None removes eviction, so a stream of new keys can keep growing the cache.
print(square.cache_parameters())
print(square.__wrapped__(5))
print(square.cache_info())
Expected output:
{'maxsize': 2, 'typed': False}
25
CacheInfo(hits=1, misses=4, maxsize=2, currsize=2)
__wrapped__ calls the original function without reading or updating the cache. It is useful for a correctness comparison, not a persistence API.
Choose a stable, hashable input contract
Lists cannot be cache keys. If a function accepts an immutable sequence of numbers, make that contract explicit with a tuple. A tuple containing a list is still unhashable. Converting a changing object into a tuple is only correct if the tuple fully captures every input that affects the result.
@lru_cache(maxsize=32)
def total(values):
return sum(values)
print(total((2, 3, 5)))
try:
total([2, 3, 5])
except TypeError as exc:
print(type(exc).__name__)
Expected output:
10
TypeError
Prefer a consistent calling convention. Positional versus keyword calls, and different keyword orders, can occupy separate entries. Use typed=True when immediate argument types must be separated; it does not recursively distinguish all types inside containers.
Invalidate results when the source changes
A cache does not notice changes to a file, database, configuration, or global variable. This small dictionary makes that stale-result problem reproducible. Clear the cache after a known change, or include a meaningful source revision in the function's arguments.
prices = {'book': 12}
@lru_cache(maxsize=16)
def price_for(sku):
return prices[sku]
print(price_for('book'))
prices['book'] = 15
print(price_for('book'))
price_for.cache_clear()
print(price_for('book'))
print(price_for.cache_info())
Expected output:
12
12
15
CacheInfo(hits=0, misses=1, maxsize=16, currsize=1)
Return immutable results when practical. A cached list or dictionary is the same object on a later hit; caller mutation can change what subsequent callers see. Exceptions raised by the function are not successful return values and are not stored as result entries.
Persistence is a separate design
lru_cache has no supported cache_update() operation and no public export/import interface for its entries. Its state disappears when the process ends. If a restart must preserve results, design a separate persistent store with a schema, source-version key, expiry policy, and validation. Do not load untrusted pickle data: deserialization can execute code. JSON is often suitable for simple records, but it still needs size and schema checks.
Concurrency and asynchronous functions
The cache's internal bookkeeping is thread-safe, but simultaneous misses may execute the wrapped function more than once. That is not an exactly-once guarantee. Do not decorate an async def with this synchronous cache: it would cache a coroutine object rather than a completed reusable result. Choose an explicitly asynchronous design if that is the requirement.
Decide whether caching belongs here
First check that results can be reused, inputs are complete and hashable, and staleness is acceptable or controlled. Then measure a representative workload with and without caching. Keep the simpler uncached function when cache overhead, low reuse, or invalidation complexity outweighs the saved work.
For the key mechanics, see the Python hash-table guide. The authoritative API and serialization limits are documented in functools.lru_cache and the pickle security warning.