Explicit, persistent caching for expensive Python functions and methods
Project description
hypercache
Explicit, persistent caching for expensive Python functions and methods.
What it does
- Caches expensive calls (API calls, embeddings, LLM generations)
- Works with sync and async methods
- Persists across restarts (disk, extensible to Redis)
- Normalizes non-hashable inputs (dicts, Pydantic models, dataclasses, bytes)
- Supports TTL, stale windows, and background refresh
Why not functools.lru_cache or cachetools
| lru_cache | cachetools | hypercache | |
|---|---|---|---|
| Async support | No | No | Yes |
| Persistent storage | No | No | Yes |
| Non-hashable inputs | No | No | Yes (normalize) |
| Instance state in key | N/A | Manual | config= |
| TTL / stale / refresh | No | TTL only | Yes |
Install
pip install hypercache
Observe cache telemetry
Any library can observe cache decisions by installing a scoped callback:
from hypercache import CachePolicy, CacheService, MemoryStore, observe_cache
cache = CacheService(MemoryStore())
events = []
with observe_cache(events.append):
cache.run(
instance="demo",
operation="embed",
version="embed:v1",
inputs={"text": "hello"},
policy=CachePolicy(),
compute=lambda: {"vector": [1, 2, 3]},
)
event = events[0]
assert event.hit is False
assert event.operation == "embed"
The observer is task-local via ContextVar, so nested async calls stay scoped to
the current request or workflow run.
Basic usage
from datetime import timedelta
from hypercache import CachePolicy, CacheService, MemoryStore, cached
def _embedder_config(self) -> dict:
return {"model": self.model, "dimensions": self.dimensions}
class Embedder:
def __init__(self, model: str = "text-embedding-3-large", dimensions: int = 1536):
self._cache = CacheService(MemoryStore(max_entries=512))
self.model = model
self.dimensions = dimensions
@cached(
version="embed:v1",
policy=CachePolicy(
ttl=timedelta(hours=6),
stale=timedelta(minutes=30),
refresh_in_background=True,
),
config=_embedder_config,
)
async def embed(self, text: str) -> dict:
return await call_embedding_api(text)
- Inputs are auto-captured from the function signature. No duplicate parameter lists.
config=explicitly declares which instance state affects the cache key. No hidden method lookups.version=lets you invalidate all cached values when the implementation changes.
Sharing config across methods
Define the config function once, reference it from multiple decorators:
def _llm_config(self) -> dict:
return {"model": self.model, "temperature": self.temperature}
class LLM:
def __init__(self, model: str, temperature: float):
self._cache = CacheService(MemoryStore())
self.model = model
self.temperature = temperature
@cached(version="generate:v1", policy=CachePolicy(), config=_llm_config)
async def generate(self, prompt: str) -> dict:
...
@cached(version="structured:v1", policy=CachePolicy(), config=_llm_config)
async def generate_structured(self, prompt: str, schema: dict) -> dict:
...
Excluding inputs from the key
Use exclude= to drop arguments that shouldn't affect caching:
@cached(
version="embed:v1",
policy=CachePolicy(),
config=_embedder_config,
exclude=frozenset({"request_id", "trace_id"}),
)
async def embed(self, text: str, request_id: str | None = None, trace_id: str | None = None):
...
Persistent cache
Swap the store — everything else stays the same:
from pathlib import Path
from hypercache import DiskCacheStore
cache = CacheService(DiskCacheStore(Path("./cache")))
Direct usage (no decorator)
result = cache.run(
instance=embedder,
operation="embed",
version="embed:v1",
inputs={"text": "hello"},
config={"model": embedder.model},
policy=CachePolicy(),
compute=lambda: embedder.embed_uncached("hello"),
)
Invalidation
Embedder.embed.key_for(embedder, "hello") # inspect key
Embedder.embed.invalidate(embedder, "hello") # delete one entry
Embedder.embed.clear(embedder) # delete all entries for this method
Design principles
- No magic: no hidden method lookups, no Protocols that silently match by name
- Explicit:
config=in the decorator, not a convention on the class - DRY: inputs auto-captured from signature, no duplicate parameter lists
- IDE-friendly: named functions, not lambdas; errors surface at import time
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file hypercache-0.2.2.tar.gz.
File metadata
- Download URL: hypercache-0.2.2.tar.gz
- Upload date:
- Size: 10.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
583e47f865585d4cc6be7b96c0530fa468bb6d755e15e8c337f911551bd85369
|
|
| MD5 |
7440a3191c58e3287bcb174f6f8444ce
|
|
| BLAKE2b-256 |
5b453f05f427c62006362ff691449c71abbc27740d6b22394322826a2303531d
|
File details
Details for the file hypercache-0.2.2-py3-none-any.whl.
File metadata
- Download URL: hypercache-0.2.2-py3-none-any.whl
- Upload date:
- Size: 12.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c3044ae79a3bb7021d46f0ce2302a86476a241f78a0e57225283727922f2bc8a
|
|
| MD5 |
ddbc7cf2d29edff5dbc98c8e6b38a268
|
|
| BLAKE2b-256 |
1ce8d9d81d62b487e367c90c1c2c3ba4ce9f69c6e4862c760c1d9a9df2ff5306
|