Build a Dead-Simple File Cache in Python (Under 50 Lines)
API rate limits killing your development speed? Here's a production-ready file cache you can drop into any Python project in 5 minutes.
The Problem
You're building something that hits an external API. During development, you call it hundreds of times. You hit rate limits. You wait. Your flow is broken.
The fix: cache API responses to disk with automatic expiration.
The Solution (47 Lines)
"""
cache.py — Drop-in file cache for any Python project.
Caches function results to JSON files with configurable TTL.
"""
import json
import time
import hashlib
from pathlib import Path
from functools import wraps
CACHE_DIR = Path(".cache")
DEFAULT_TTL = 300 # 5 minutes
def cached(ttl: int = DEFAULT_TTL, key_prefix: str = ""):
"""
Decorator that caches function results to disk.
Usage:
@cached(ttl=600)
def fetch_data(url):
return requests.get(url).json()
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Generate a unique cache key from function name + arguments
raw_key = f"{key_prefix or func.__name__}:{args}:{kwargs}"
cache_key = hashlib.md5(raw_key.encode()).hexdigest()
cache_file = CACHE_DIR / f"{cache_key}.json"
# Check cache
if cache_file.exists():
data = json.loads(cache_file.read_text())
age = time.time() - data["ts"]
if age < ttl:
return data["result"]
# Cache miss — call the function
result = func(*args, **kwargs)
# Write to cache
CACHE_DIR.mkdir(exist_ok=True)
cache_file.write_text(json.dumps({
"ts": time.time(),
"key": raw_key,
"result": result
}, default=str))
return result
return wrapper
return decorator
def clear():
"""Clear all cached data."""
if CACHE_DIR.exists():
for f in CACHE_DIR.glob("*.json"):
f.unlink()
def stats():
"""Show cache statistics."""
if not CACHE_DIR.exists():
return {"files": 0, "size_bytes": 0}
files = list(CACHE_DIR.glob("*.json"))
total_size = sum(f.stat().st_size for f in files)
valid = 0
expired = 0
for f in files:
data = json.loads(f.read_text())
if time.time() - data["ts"] < DEFAULT_TTL:
valid += 1
else:
expired += 1
return {
"total_files": len(files),
"valid": valid,
"expired": expired,
"size_kb": round(total_size / 1024, 1)
}
How to Use It
Basic Usage
import requests
from cache import cached
@cached(ttl=300) # Cache for 5 minutes
def get_weather(city: str):
resp = requests.get(f"https://api.weather.com/{city}")
return resp.json()
# First call: hits the API
data = get_weather("london")
# Second call within 5 min: instant, from disk
data = get_weather("london")
Custom TTL Per Function
@cached(ttl=3600) # Cache for 1 hour
def get_exchange_rates():
return requests.get("https://api.rates.com/latest").json()
@cached(ttl=60) # Cache for 1 minute (prices change fast)
def get_stock_price(ticker: str):
return requests.get(f"https://api.stocks.com/{ticker}").json()
Clear Cache When Needed
from cache import clear, stats
# Check what's cached
print(stats())
# {'total_files': 12, 'valid': 8, 'expired': 4, 'size_kb': 45.2}
# Wipe everything
clear()
Why This Works Better Than In-Memory Caching
| Feature | In-Memory (dict) | File Cache |
|---|---|---|
| Survives restart | No | Yes |
| Works across processes | No | Yes |
| Zero dependencies | Yes | Yes |
| Speed | Faster | Fast enough |
| Inspectable | No | Yes (JSON files) |
The file cache is slightly slower than a dict, but it survives process restarts and works across multiple scripts — which matters way more during development.
Production Hardening (Optional)
For production use, add these:
import fcntl
def _atomic_write(path: Path, content: str):
"""Write atomically to avoid corruption from concurrent access."""
tmp = path.with_suffix(".tmp")
tmp.write_text(content)
tmp.rename(path) # Atomic on most filesystems
def _auto_cleanup(max_files: int = 1000):
"""Remove oldest cache files if cache grows too large."""
files = sorted(CACHE_DIR.glob("*.json"),
key=lambda f: f.stat().st_mtime)
while len(files) > max_files:
files.pop(0).unlink()
When NOT to Use This
- High-frequency reads (>1000/sec): Use Redis or memcached instead
- Large responses (>10MB): Use SQLite or a proper database
- Shared servers: File locking gets complicated; use Redis
- Sensitive data: Cache files are plaintext JSON on disk
For everything else — development, prototyping, small production services — this 47-line file cache is all you need.
TL;DR
# Copy cache.py into your project
# Decorate any function with @cached(ttl=300)
# Never hit rate limits during development again
Grab the full code from the snippet above. No pip install required.
Found this useful? Share it with a dev who's tired of rate limits.
Top comments (0)