Python Caching — Speed Up Your Applications
Caching stores results of expensive operations to avoid recomputation. A cache hit is 1000x faster than recomputing.
Learning Objectives
- Use functools.lru_cache for memoization
- Implement in-memory caching with TTL
- Integrate Redis for distributed caching
- Apply cache invalidation strategies
Why Cache?
Without Cache:
Request → Database Query (100ms) → Response
Request → Database Query (100ms) → Response
Request → Database Query (100ms) → Response
Total: 300ms
With Cache:
Request → Cache Hit (1ms) → Response
Request → Cache Hit (1ms) → Response
Request → Cache Miss → Database (100ms) → Cache Store → Response
Total: 103ms (3x faster!)
functools.lru_cache
The easiest way to add caching — just one decorator.
from functools import lru_cache
@lru_cache(maxsize=128) # Cache up to 128 results
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
# First call: computes recursively (slow)
print(fibonacci(100)) # Fast!
# Subsequent calls: returns cached result (instant)
print(fibonacci.cache_info())
# CacheInfo(hits=98, misses=101, maxsize=128, currsize=101)
# Clear cache
fibonacci.cache_clear()
TTL Cache (Time-To-Live)
from functools import lru_cache
from datetime import timedelta
from cachetools import TTLCache
# cachetools provides TTL support
cache = TTLCache(maxsize=100, ttl=300) # 5 minute TTL
def get_user(user_id):
if user_id in cache:
return cache[user_id]
# Expensive database query
user = db.query(f"SELECT * FROM users WHERE id = {user_id}")
cache[user_id] = user
return user
In-Memory Cache with TTL
import time
from typing import Any, Optional
class SimpleCache:
def __init__(self, default_ttl: int = 300):
self.cache = {}
self.default_ttl = default_ttl
def get(self, key: str) -> Optional[Any]:
if key in self.cache:
value, timestamp = self.cache[key]
if time.time() - timestamp < self.default_ttl:
return value
del self.cache[key] # Expired
return None
def set(self, key: str, value: Any, ttl: int = None):
self.cache[key] = (value, time.time())
def delete(self, key: str):
self.cache.pop(key, None)
def clear(self):
self.cache.clear()
Redis Caching
import redis
import json
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
def cache_result(key: str, func, ttl: int = 300):
"""Cache a function result in Redis."""
cached = r.get(key)
if cached:
return json.loads(cached)
result = func()
r.setex(key, ttl, json.dumps(result))
return result
# Usage
user_data = cache_result(
"user:123",
lambda: fetch_user_from_db(123),
ttl=600
)
Cache Invalidation Strategies
# 1. Time-based (TTL) — simplest
# Data expires after N seconds
# 2. Event-based — invalidate on change
def update_user(user_id, data):
db.update(user_id, data)
r.delete(f"user:{user_id}") # Invalidate cache
# 3. Pattern-based — invalidate groups
def invalidate_pattern(pattern):
keys = r.keys(pattern)
if keys:
r.delete(*keys)
invalidate_pattern("user:*") # Clear all user caches
Key Takeaways
lru_cacheis the easiest way to add caching- Use TTL (time-to-live) for cache expiration
- Redis enables distributed caching across servers
- Cache invalidation is hard — choose strategies carefully
- Cache expensive computations, not simple lookups
- Monitor cache hit ratio (aim for greater than 80%)
- Use
@lru_cachefor recursive functions - Consider cache warming for critical data