The functools.cache decorator in the Python standard library (added in Python 3.9) automatically memoizes a function: it stores the result of every call and returns the stored result on subsequent calls with the same arguments. This turns repeated expensive computations into near-instant dictionary lookups. It is one of several tools covered in functional programming with functools.
from functools import cache
import time
@cache
def expensive_calculation(n):
time.sleep(1)
return n * nWith the function decorated, call it twice with the same argument and time each call to see the difference caching makes:
start = time.time()
print(expensive_calculation(42))
print(f"First call: {time.time() - start:.2f}s")
start = time.time()
print(expensive_calculation(42))
print(f"Second call: {time.time() - start:.4f}s")The first call pays the full one-second cost of the sleep, but the second call with the same argument (42) skips straight to the cached result:
1764
First call: 1.00s
1764
Second call: 0.0001sThe first call takes a full second. The second call with the same argument returns instantly from the cache. No code changes beyond adding @cache were needed.
How caching works
When you decorate a function with @cache, Python creates an internal dictionary mapping argument tuples to return values, so understanding what counts as a distinct key is essential to using it correctly.
from functools import cache
@cache
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
print(greet("Maya")) # Hello, Maya!
print(greet("Maya", "Hello")) # Hello, Maya!
print(greet("Maya", "Hi")) # Hi, Maya!greet("Maya") and greet("Maya", "Hello") produce the same result but are separate cache entries because the argument tuples differ. The cache key includes all positional and keyword arguments.
lru_cache: bounded caching
functools.lru_cache limits the cache to a maximum number of entries. When the cache is full, the least recently used entry is evicted.
from functools import lru_cache
call_log = []
@lru_cache(maxsize=3)
def square(n):
call_log.append(n)
return n * nCall square with a sequence that cycles through more distinct values than the cache can hold, then inspect which calls actually ran:
for x in [1, 2, 3, 4, 1, 2, 5]:
square(x)
print("Actual computations:", call_log)
print(square.cache_info())Every one of the 7 calls shows up in call_log, confirming that none of them actually hit the cache with this cache size:
Actual computations: [1, 2, 3, 4, 1, 2, 5]
CacheInfo(hits=0, misses=7, maxsize=3, currsize=3)With maxsize=3, only three results are cached at a time. By the time 4 is computed, the cache is full and the oldest entry (1) is evicted, so the later square(1) and square(2) calls both miss and must be recomputed. A small maxsize like this defeats the purpose of caching if the same values keep cycling in and out.
| Parameter | cache | lru_cache(maxsize=N) |
|---|---|---|
| Cache limit | Unlimited | N entries |
| Eviction policy | None | Least recently used |
| Memory risk | Can grow unbounded | Bounded |
| Best for | Few unique inputs | Many unique inputs |
Inspecting cache statistics
Both decorators provide .cache_info() for monitoring performance, which is useful for confirming a cache is actually paying off in a running application.
from functools import lru_cache
@lru_cache(maxsize=128)
def fetch(key):
return f"data_{key}"
for k in [1, 2, 1, 3, 2, 1]:
fetch(k)
info = fetch.cache_info()
print(f"Hits: {info.hits}") # Hits: 3
print(f"Misses: {info.misses}") # Misses: 3
print(f"Current size: {info.currsize}") # Current size: 3
print(f"Max size: {info.maxsize}") # Max size: 128A hit is a cached return. A miss required actual computation. Monitor these in long-running applications to verify the cache is effective.
Clearing the cache
Call .cache_clear() to invalidate all cached entries.
from functools import cache
@cache
def get_config(key):
print(f"Loading {key}")
return {"key": key, "value": 42}
print(get_config("db"))
print(get_config("db"))
get_config.cache_clear()
print(get_config("db"))"Loading db" only prints on the first call and again after cache_clear(), since the second call in between is served straight from the cache:
Loading db
{'key': 'db', 'value': 42}
{'key': 'db', 'value': 42}
Loading db
{'key': 'db', 'value': 42}After cache_clear(), the next call recomputes the result. Use this when underlying data changes or between test cases.
When caching fails
Caching requires hashable arguments and pure functions.
from functools import cache
@cache
def broken(data):
return len(data)
broken([1, 2, 3])Passing a list as an argument raises an exception immediately, before the function body even runs, because @cache tries to use it as a dictionary key:
TypeError: unhashable type: 'list'Lists, dictionaries, and sets are not hashable. Convert them to tuples or frozensets before passing.
Caching also requires the function to be pure: same input always produces the same output. Do not cache functions that depend on external state (current time, database contents, random values) unless you clear the cache when that state changes.
from functools import cache
from datetime import datetime
@cache
def current_hour():
return datetime.now().hourThis function returns the same value forever after the first call, which is wrong. Only cache functions whose results are determined solely by their arguments.
Typed caching
Pass typed=True to lru_cache to treat arguments of different types as separate cache entries.
from functools import lru_cache
@lru_cache(maxsize=128, typed=True)
def double(value):
return value * 2
print(double(3)) # 6
print(double(3.0)) # 6.0With typed=True, double(3) and double(3.0) are cached separately. Without it, they share a cache entry because 3 == 3.0.
Practical example: recursive Fibonacci
The classic example where caching makes an exponential algorithm linear.
from functools import cache
@cache
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(200))Without caching, this call alone would take longer than the age of the universe to finish. With @cache, it returns instantly because each fibonacci(n) is computed exactly once:
280571172992510140037611932413038677189525Without caching, fibonacci(200) would make an astronomical number of recursive calls. With @cache, each fibonacci(n) is computed exactly once, reducing the total work from O(2^n) to O(n).
Common mistakes
Caching functions with side effects. If a function writes to a file, sends a network request, or modifies global state, caching it changes program behavior. Only cache pure functions.
Using cache on methods without considering memory. Each instance creates its own cache. If thousands of short-lived instances are created, unbounded cache can leak memory. Use lru_cache(maxsize=...) for methods.
Caching functions that return mutable objects. If a cached function returns a list and the caller modifies it, the cached list is also modified. Return immutable values (tuples, frozensets) or defensive copies from cached functions.
Forgetting to clear the cache in tests. Cached results persist across test cases. Call func.cache_clear() in test setup or teardown to ensure test isolation.
Rune AI
Key Insights
@functools.cachememoizes all function calls with no size limit.@functools.lru_cache(maxsize=N)limits the cache to N entries, evicting the least recently used.- Access
.cache_info()for hits, misses, and current cache size. - Call
.cache_clear()to invalidate the entire cache. - All arguments must be hashable; unhashable types like lists cause a
TypeError. - Caching works best on pure functions where the same input always produces the same output.
Frequently Asked Questions
When should I use functools.cache vs lru_cache?
Can I cache methods on class instances?
How do I clear a cache?
Conclusion
functools.cache and lru_cache are the simplest way to add memoization to Python functions. Add one decorator and repeated calls with the same arguments return instantly. Use cache for simple cases, lru_cache when you need size limits or performance statistics, and always call cache_clear() when cached data becomes stale.
More in this topic
Format Dates and Times in Python
Learn how to use strftime and strptime to format dates as strings and parse date strings into Python datetime objects.
What Is the Python Standard Library?
Learn what the Python standard library is, why it matters, and how to use its built-in modules without installing anything extra.
Encode and Decode Base64 Data in Python
Learn how to use Python's base64 module to encode binary data as text and decode Base64 strings back to bytes.