Caching Function Results with functools
Learn when function-result caching is useful, how functools.cache and lru_cache reuse results, how to inspect or clear a cache, and which functions should not be cached.
Intermediate
When a function receives the same input again, Python normally runs its body again. Function-result caching remembers a result for a set of arguments so a later matching call can reuse it. This technique is often called
memoization. It is useful when the work is worth reusing, but it is not a general speed switch for every function.
Cache only results that can be reused
The
functools module provides
@cache for an unbounded cache and
@lru_cache for a cache that can limit its size. Both are decorators: they return a callable that looks up previous results before deciding whether the wrapped function needs to run.
Output
Ready to run.
The first call records a miss and computes the normalized string. The second call uses the same argument, so it is a cache hit. The function is deliberately deterministic: the same input always produces the same result.
Choose between cache and lru_cache
@cache is the convenient choice when retaining every distinct argument/result pair is acceptable.
@lru_cache(maxsize=...) keeps only a bounded number of recent entries. When the cache is full, an older least-recently-used entry can be evicted to make room for a new one.
Output
Ready to run.
A bounded cache is often a safer default for long-running programs with many possible inputs. The
cache_info() result reports hits, misses, the capacity, and the current number of entries; use those measurements rather than guessing whether caching helps.
Quick check
Which decorator creates a cache with no entry-count limit?
from functools import cache, lru_cache
@
def label(value):
return value.upper()
label("bread")
print(label.cache_info().maxsize)
Clear results when their inputs no longer describe reality
A cache does not know when a database, configuration dictionary, file, or other source of information changes. If an answer depends on changeable external state, a stored result can become stale. The wrapped function provides
cache_clear() to discard its stored entries when you have a clear reason to invalidate them.
Output
Ready to run.
This example is intentionally tied to external data to show the invalidation problem. In production, prefer a design where the function’s result follows from its arguments alone, or make cache invalidation an explicit part of the program.
Quick check
Which expression clears the old rate before reading the updated value?
from functools import cache
rates = {"north": 4.0}
@cache
def rate(region):
return rates[region]
rate("north")
rates["north"] = 5.0
print()
Know the limits before adding a cache
Cache arguments must be hashable because the cache uses them as dictionary keys. A list or dictionary cannot be used directly as a cached argument; a tuple of hashable values may work instead. Distinct argument patterns may produce distinct entries, even when they look equivalent to a person.
Quick check
Which value is rejected because it cannot be hashed for a cache key?
from functools import cache
@cache
def count_items(items):
return len(items)
def describe(value):
try:
count_items(value)
except TypeError:
return "unhashable"
return type(value).__name__
print(describe())
Do not cache functions when each call must perform a side effect, read the current time, generate fresh random data, or return a newly independent mutable object. Cached return values are reused too, so callers can accidentally share and mutate one list or dictionary. Caching generators and asynchronous functions is also usually inappropriate because those calls are expected to create fresh execution objects.
Output
Ready to run.
The list example is a deliberate warning, not a recommended pattern: both calls receive the same cached list object. Use caching for stable, reusable results, and keep caching out when object freshness or side effects are part of the function’s contract.
Check whether the cache is helping
Use
cache_info() to inspect reuse and
cache_clear() to reset it. Caching trades memory and extra lookup work for avoiding repeated computation, so measure it with representative inputs. A cache can preserve argument and result objects in memory until entries are evicted or cleared.
Output
Ready to run.
Keep the ownership boundaries clear: recursion explains how calls solve smaller versions of a problem, the Decorators lesson explains decorator mechanics, and this lesson owns when to cache function results and how to use the standard-library cache APIs.
Quick check
Before adding a cache, ask three things: are the arguments hashable, can the result safely be reused, and is there a deliberate policy for stale entries and memory use? Then test cache hits and misses with realistic calls.
When you are ready, take the quiz and complete the separate function-caching coding challenges.
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.