Table of Contents
Python LRU cache
Python LRU cache (from functools.lru_cache) memoizes function results in a fixed-size cache, keeping the most-recently-used items and evicting least-recently-used ones when full. Caching expensive computations (recursive functions, database queries, calculations) can dramatically improve performance—but only for functions with pure (no side effects) logic and hashable arguments.
Use LRU cache for expensive pure functions; be careful caching functions with side effects or mutable arguments.
Example
This example shows LRU caching speeding up expensive computations.
# run: python3 lru_cache.py # description: function memoization with LRU cache from functools import lru_cache import time # Expensive fibonacci without caching def fib_slow(n): if n <= 1: return n return fib_slow(n - 1) + fib_slow(n - 2) # Cached version @lru_cache(maxsize=128) def fib_fast(n): if n <= 1: return n return fib_fast(n - 1) + fib_fast(n - 2) # Compare performance print("Computing fib(30) without cache...") start = time.time() result = fib_slow(30) elapsed = time.time() - start print(f"Result: {result}, Time: {elapsed:.3f}s") print("\nComputing fib(30) with cache...") start = time.time() result = fib_fast(30) elapsed = time.time() - start print(f"Result: {result}, Time: {elapsed:.6f}s") # Cache info print(f"\nCache info: {fib_fast.cache_info()}") # Clearing cache fib_fast.cache_clear() print(f"After clear: {fib_fast.cache_info()}") # Expensive computation (database lookup simulation) @lru_cache(maxsize=32) def lookup_user(user_id): """Simulated expensive database lookup.""" print(f" (looking up user {user_id}...)") time.sleep(0.1) return f"User {user_id}" print("\n" + "="*60) print("User lookups:") print(lookup_user(1)) print(lookup_user(2)) print(lookup_user(1)) # cached, no lookup print(lookup_user(2)) # cached, no lookup print(f"\nCache hits: {lookup_user.cache_info().hits}") print(f"Cache misses: {lookup_user.cache_info().misses}") # With custom type hints (Python 3.9+) @lru_cache(maxsize=64) def expensive_computation(x: int, y: int) -> int: """Pure function suitable for caching.""" return x ** y print(f"\n{expensive_computation(2, 10)}") print(f"Cache size: {expensive_computation.cache_info().currsize}") # Disable cache (maxsize=None for unlimited, use carefully!) @lru_cache(maxsize=None) def unlimited_cache(n): """Unlimited cache (grows forever).""" return n * 2 # Disable caching @lru_cache(maxsize=0) def no_cache(n): """Cache disabled (no memoization).""" return n * 2
Common patterns
Basic usage:
@lru_cache(): decorate function to cache results@lru_cache(maxsize=128): max cached entries (default 128)- Cached by argument values; arguments must be hashable
Cache info:
func.cache_info(): returns CacheInfo(hits, misses, maxsize, currsize)- Useful for monitoring cache effectiveness
func.cache_clear(): empty the cache
Hashable arguments only:
- Works with ints, strings, tuples
- Won't work with lists, dicts, sets (unhashable)
- For mutable args, convert to immutable: tuple instead of list
Side effects and purity:
- Caching only safe for pure functions (same output for same input)
- Don't cache functions that write files, modify state, etc.
- Don't cache functions with time-dependent results
Maxsize tuning:
- Small maxsize: less memory, more cache misses
- Large maxsize: more memory, fewer misses
maxsize=None: unlimited cache (grows forever; use carefully)maxsize=0: caching disabled (decorator works but doesn't cache)
Performance trade-off:
- Cache lookup has overhead; not always faster
- Only beneficial for expensive functions
- Worst case: cache lookup overhead with no hits
- Test with cache_info to measure
Custom caching (when LRU insufficient):
functools.cache()(Python 3.9+): unlimited, simplerfunctools.cached_property: cache class property- Roll custom cache for complex requirements
Thread-safety:
- LRU cache is thread-safe (uses locks)
- Slight performance cost for thread safety
- Large multi-threaded workloads: consider alternatives
Typed version:
@lru_cache()cachesf(1)andf(1.0)the same way@functools.lru_cache(typed=True)treats as different (Python 3.8+)- Use when int and float distinctions matter
Common mistakes:
- Caching pure functions that shouldn't be: if results are correct, it's fine
- Not checking cache_info(); could be hurting performance
- Caching huge objects; monitor memory usage
- Arguments with large hash tables (custom hash expensive)
