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:

Cache info:

Hashable arguments only:

Side effects and purity:

Maxsize tuning:

Performance trade-off:

Custom caching (when LRU insufficient):

Thread-safety:

Typed version:

Common mistakes: