Python caching stores the result of an expensive function call so the next call with the same arguments returns instantly. Python's functools module gives you two decorators for this: cache and lru_cache. Both turn a function into a lookup table, but they differ in how they manage memory.
Use caching when a function is called many times with the same inputs. Classic cases are recursive algorithms like Fibonacci, repeated database-style lookups in memory, or any pure computation where the same arguments reappear. The first call pays the full cost, and every subsequent call is free.
Caching does not help when arguments are always different. A cache that never hits is just wasted memory. Profile first to confirm the function is actually called often enough with repeated arguments for a cache to pay off, as covered in the article on common Python performance bottlenecks.
The simplest cache: functools.cache
The cache decorator is the no-config option. It remembers every call forever. Python 3.9 added it as a simpler alternative to lru_cache with no size limit.
from functools import cache
@cache
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(100))Without the decorator, fibonacci(100) makes over a trillion recursive calls and never finishes. With cache, each value from 0 to 100 is computed exactly once. The function becomes a fast lookup table.
The tradeoff: cache never forgets. If your function has a large or unbounded argument space, the cache grows forever. Use it when the set of possible arguments is small and known, like a few hundred values at most.
Controlling size with lru_cache
lru_cache adds a maxsize parameter. When the cache is full, the least recently used entry is removed. This keeps memory bounded.
from functools import lru_cache
@lru_cache(maxsize=128)
def fetch_user(user_id):
# Simulate an expensive lookup.
result = compute_user_data(user_id)
return resultThe first 128 unique user IDs fill the cache. When user 129 arrives, the least recently accessed entry is evicted. This pattern fits API response caching, computed properties, and any scenario where recent arguments are likely to repeat.
Pick maxsize based on how many distinct arguments you realistically expect to repeat, not an arbitrary power of 2. Common values are 128, 256, or 512. Setting maxsize=None makes it behave like cache with no limit.
Seeing cache stats
Both decorators expose a cache_info method that tells you how well the cache is working.
@lru_cache(maxsize=128)
def square(n):
return n * n
for i in range(50):
square(i) # first call: miss
square(i) # second call: hit
print(square.cache_info())
# CacheInfo(hits=50, misses=50, maxsize=128, currsize=50)Hits are calls that returned a cached value. Misses are calls that computed a new value.
A high hit ratio means the cache is working. A low ratio means arguments rarely repeat and the cache is wasting memory.
You can also clear the cache with cache_clear():
square.cache_clear()
print(square.cache_info())
# CacheInfo(hits=0, misses=0, maxsize=128, currsize=0)Clearing is useful in tests or when the underlying data changes and cached values become stale.
What makes a function cacheable
Only pure functions should be cached. A pure function always returns the same result for the same arguments and has no side effects.
Do cache functions that do CPU-heavy math, string processing, or data transformation where arguments repeat. Do not cache functions that read files, query databases, depend on mutable global state, or return different results for the same arguments.
# Good: pure computation with repeated arguments
@cache
def expensive_math(x, y):
return sum(i ** 0.5 for i in range(x, y))
# Bad: depends on external state
@cache
def read_config(key):
return config[key] # config may changeIf the external state changes, the cache returns stale data. For those cases, use cache_clear to invalidate the cache when the state changes.
Caching with typed arguments
lru_cache has a typed parameter. When typed=True, arguments that compare equal but have different types are cached separately.
@lru_cache(maxsize=128, typed=True)
def double(x):
return x * 2
print(double(1)) # 2
print(double(1.0)) # 2.0, cached separately from double(1)Without typed=True, double(1) and double(1.0) share the same cache entry because 1 == 1.0 is True in Python. With typed=True, they are distinct entries. This matters when the return type depends on the argument type.
Caching in practice: a before-and-after
Consider a function that counts word frequencies in a text corpus. Without caching, the same document might be parsed multiple times.
@lru_cache(maxsize=64)
def word_counts(doc_id):
text = load_document(doc_id)
counts = {}
for word in text.split():
counts[word] = counts.get(word, 0) + 1
return countsThe first call to word_counts("chapter-1") parses the document. Every subsequent call to word_counts("chapter-1") returns the cached dictionary instantly. With maxsize=64, the 64 most recently accessed documents stay cached.
If you have a batch process that calls the function thousands of times for a fixed set of documents, the cache hit rate approaches 100 percent after the first pass. The speedup is dramatic for I/O-heavy functions like this.
For loop-level speed improvements, the article on optimizing Python loops and iterations covers techniques that pair well with caching. When the bottleneck is slow repeated computation, caching and loop optimization together can deliver order-of-magnitude improvements.
Rune AI
Key Insights
- functools.cache memoizes every call without a size limit; use it when the argument space is small and fixed.
- lru_cache(maxsize=N) limits memory and evicts the least recently used entry when full.
- Caching helps most with pure functions that are called repeatedly with the same arguments.
- Never cache functions with side effects or external dependencies.
- Always measure to confirm the cache hit rate justifies the memory cost.
Frequently Asked Questions
What is the difference between functools.cache and lru_cache?
When should I NOT use caching in Python?
Conclusion
Caching with functools turns repeated expensive calls into instant lookups. Use cache for functions with a fixed small argument space. Use lru_cache with a sensible maxsize when arguments are unbounded. Always verify the cache is actually helping by measuring before and after. A cache that never hits is just wasted memory.
More in this topic
How to Test Python Code
Learn what testing means in Python, the core ideas behind automated tests, and the built-in and third-party tools that make testing practical.
Write Your First Python Unit Test
Write and run your first Python unit test step by step, using unittest to check a small function and understand the test output.
The Factory Pattern in Python
Learn how to use the factory pattern in Python to encapsulate object creation, making your code more flexible and easier to extend.