Development-time persistent caching for function calls and variables, to speed up iteration and save money on APIs.
Project description
devstash
devstash is a development-time utility for persistent caching of function return values across multiple program executions.
When you’re iterating on code, you often hit the same slow lines (e.g. heavy computations, file parsing, data processing) or expensive lines (e.g. LLM requests that cost tokens/money). With devstash, you can mark those lines once and cache the results on disk — so the next run reuses the cached values instead of re-executing them.
That means:
- 🚀 Faster iteration while debugging or prototyping
- 💸 Save money by skipping repeated LLM/API calls
- 🧘 No wasted time waiting for recomputation during development
- 🌐 Offline development after the first run, since cached API/web results are replayed without needing network access
- 🧪 Deterministic results for easier debugging — results are identical every run
- 🧰 Mock-friendly cache files that can be reused as test data, eliminating the need to hit real APIs or recompute fixtures
- 🔍 Transparent storage in a
.devstash_cache/folder — easy to inspect, clear, or share - 👥 Team-ready: share cached results across machines to save setup time
Table of Contents
⚡ Quickstart
Install and run in seconds:
pip install devstash
import time
import devstash
devstash.activate() # ✅ enable caching for this run
def slow_function(x):
print("Running slow_function...")
time.sleep(10)
return x * 2
val = slow_function(10) # @devstash
print(val)
💡 First run: prints “Running slow_function…” and caches the result.
💡 Subsequent runs: instantly reuses the cached value without executing the function.
✨ Features
- Cache function return values with a simple inline marker (
# @devstash) - Argument-sensitive caching: separate cache entries are created for different function arguments and keyword arguments.
- Safe file handling: cache filenames are sanitized to avoid injection or invalid filename issues, and truncated to avoid OS filename length limits.
- Transparent disk storage in a
./.devstash_cache/folder - Automatic restore: cached values are re-injected into your program on the next run
- Logging integration: view caching activity with Python’s logging system
- Zero dependencies (just Python stdlib)
- Optional TTL (time-to-live) to expire cache after a given time (e.g.
30m,2h,1d,1w) - Command-line tools: manage cache files with ease (list, clear, inspect).
🔥 Use Cases
🧑🔬 Expensive LLM calls
from openai import OpenAI
import devstash
devstash.activate()
client = OpenAI()
prompt = "Summarize War and Peace in 3 sentences."
summary = client.chat.completions.create( # @devstash
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}]
)
print(summary.choices[0].message["content"])
📊 Large file parsing
from langchain_community.document_loaders import PyPDFLoader
import devstash
devstash.activate()
loader = PyPDFLoader("A_LARGE_PDF_DOCUMENT.pdf")
docs = loader.load() # @devstash
print(f"Number of docs: {len(docs)}")
🧮 Machine learning
import logging, time
from sklearn.datasets import fetch_openml
from sklearn.decomposition import PCA
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.model_selection import train_test_split
import devstash
devstash.activate()
start = time.time()
X, y = fetch_openml("mnist_784", version=1, return_X_y=True, as_frame=False, parser="liac-arff") # @devstash
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) # @devstash
pipe = make_pipeline(PCA(n_components=50), LogisticRegression(max_iter=2000))
pipe.fit(X_train, y_train) # @devstash
acc = pipe.score(X_test, y_test) # @devstash
print(f"Accuracy: {acc:.3f}")
print("Program execution time: %.2f s" % (time.time() - start))
# Output from the first run when the cache is cold:
# Accuracy: 0.908
# Program execution time: 29.67 s
# Output from the second run when the cache is warm:
# Accuracy: 0.908
# Program execution time: 0.68 s
💾 API responses
import requests
import devstash
devstash.activate()
url = "https://api.github.com/repos/langchain-ai/langchain"
resp = requests.get(url) # @devstash ttl=24h
repo_info = resp.json()
print(repo_info["stargazers_count"])
⏳ Cache with Time-To-Live (TTL)
You can add an optional ttl parameter to your # @devstash marker.
TTL values can be expressed in seconds (s), minutes (m), hours (h), days (d), or weeks (w).
Examples: 30m, 2h, 1d, 1w.
import devstash, requests
devstash.activate()
url = "https://api.github.com/repos/langchain-ai/langchain"
# Cache result for 1 day, then refresh
resp = requests.get(url) # @devstash ttl=1d
print(resp.json()["stargazers_count"])
- First run: makes the API call and caches the result.
- Subsequent runs within 1 day: loads directly from cache.
- After 1 day: re-fetches and updates the cache automatically.
🛠️ How It Works
devstash works by transforming your program at runtime:
- Explicit activation: When you call
devstash.activate(), it reads your main script (the entrypoint insys.argv[0]). - Build an AST: The code is parsed into an Abstract Syntax Tree (AST), a structured representation of your Python source.
- Rewrite annotated lines: Function calls marked with
# @devstashare rewritten to wrap them with the persistent cache helper. - Compile and exec: The rewritten AST is compiled back into Python bytecode and executed in a fresh
__main__namespace. - Persistent storage: Values are stored on disk using pickle, and automatically restored on subsequent runs.
- TTL support: Before reading a cache file, devstash checks its last-modified time. If the file is older than the specified TTL, the cache is refreshed.
- Safe filenames: All cache filenames are sanitized to prevent injection and truncated to fit within common OS filename length limits.
👉 If you don’t want rewriting in a certain environment, set:
export DEVSTASH_SKIP_REWRITE=1
🖥️ CLI Tools
devstash includes a simple CLI for managing your cache:
$ devstash list --ttl 1m [19:49:21]
Cache File Age TTL Status
------------------------------------------------------------------------------------------------------------------------
openai.resources.embeddings__Embeddings.create__1e53b9c3f5768615.pkl 5m 8s expired (>1m)
langchain_core.document_loaders.base__BaseLoader.load__d019ca77b2cac706.pkl 5s valid (<=1m)
sklearn.model_selection._split__train_test_split__17b58b2de4f4cff9.pkl 13s valid (<=1m)
requests.api__get__0c4579007009458c.pkl 5m 13s expired (>1m) 4m 48s valid
Lists all cached files, including their size, age, and TTL if applicable.
$ devstash clear clear --pattern openai.resources.embeddings__Embeddings.create__1e53b9c3f5768615.pkl
Removed 1 cache file(s).
$ devstash clear --all [19:43:00]
Cleared all cached entries.
Clears out files from the ./.devstash_cache/ directory.
These commands help inspect or reset caches without manually navigating files.
📚 Related Work
There are several existing Python libraries that provide caching or mocking functionality, but devstash takes a different approach designed for day-to-day development convenience.
| Tool | Approach | Pros | Cons |
|---|---|---|---|
| persist-cache | Decorator-based persistent cache | ✅ Easy to apply with decorators | ❌ Requires decorating functions, not inline caching. Cannot decorate external library functions |
| joblib | Memory.cache() decorator |
✅ Great for machine learning pipelines | ❌ Requires explicit wrapping/decorating |
| diskcache | Disk-backed dictionary/decorators | ✅ Powerful and flexible | ❌ More boilerplate, extra setup |
| vcrpy | Records/replays HTTP requests | ✅ Excellent for offline API testing | ❌ Only works for HTTP calls |
| pytest-cache | pytest-specific cache | ✅ Useful in test environments | ❌ Limited to pytest, not general dev |
| devstash | Inline # @devstash marker |
✅ Zero-boilerplate, works (almost) anywhere, argument-sensitive caching | ❌ Development-only, not for production |
🔑 How devstash is different
Unlike the above, devstash focuses on zero-boilerplate caching during development.
- Just add
# @devstashto a line of code. - No decorators, wrappers, or test frameworks required.
- Works with any function return value that is pickle-serializable.
- Optimized for saving time and cost during iterative coding, not for production.
⚠️ Notes & Limitations
- devstash is designed for development/debugging only, not for production caching.
- Cached objects must be pickle-serializable.
- Cache invalidation: delete
./.devstash_cache/if values become stale. - Function chaining is not supported.
E.g. to avoid an API call inrequests.get(url).json()you must split the.json()onto a separate line and apply the marker to the.get()call. - TTL support: you can specify cache expiry with
ttl=...in the marker (# @devstash ttl=30m). Invalid TTL formats will raise an error. Cache freshness is determined using the file’s last modified time. - Execution context support: devstash currently only supports being run with the main Python executable (e.g.
python script.py) or through package manager wrappers like uv (uv run script.py) or poetry (poetry run python script.py).
Other runner-style tools such asflask run,uvicorn, orgunicornare not yet supported because they import your application as a module instead of executing it as the entrypoint.
Support for these wrappers is planned for the future. For now, always run your scripts directly withpython(oruv run/poetry run).
🤝 Contributing
Contributions, feedback, and ideas are very welcome!
- 🐛 Found a bug? Please open an issue with details so we can fix it.
- 💡 Have a feature idea? Share it in the issues or discussions.
- 🔧 Want to contribute code? Fork the repo, create a branch, and open a pull request.
- 📖 Improve documentation? Edits and clarifications are always appreciated.
🔨 Development Setup
This project uses uv and Ruff for dependency management and linting.
Install dependencies:
uv sync
Run tests:
uv run pytest
Run Ruff checks:
uv run ruff check .
Automatically fix issues:
uv run ruff check . --fix
Format code:
uv run ruff format .
📦 Release Process
This project uses Semantic Versioning.
Releases are driven by git tags: pushing a tag will automatically trigger the GitHub Actions workflow to publish to PyPI and create a GitHub Release.
To bump the package version, update the changelog and push a tag, run the ./release.sh script.
devstash is still evolving, and community input will help shape its direction. Whether it’s catching rough edges, improving performance, or adding new caching strategies — we’d love your help!
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 devstash-0.1.1.tar.gz.
File metadata
- Download URL: devstash-0.1.1.tar.gz
- Upload date:
- Size: 19.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.22
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
21b2579eda41bd44d071b52f746730731e2f07df3ff085c77548683e01077260
|
|
| MD5 |
39930e753341f8158dfbd1df70870879
|
|
| BLAKE2b-256 |
555be9752905aff444b2823cb934f2deab8c81ede08c7eb04ded817b02d80301
|
File details
Details for the file devstash-0.1.1-py3-none-any.whl.
File metadata
- Download URL: devstash-0.1.1-py3-none-any.whl
- Upload date:
- Size: 13.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.22
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a025f63f8bef5b661aa3ed3973a97cf02ecdd2da10029cb7309cb6d135c87bd8
|
|
| MD5 |
02efc3be35bbdf2a8aa881c8e819391d
|
|
| BLAKE2b-256 |
31a78dfb0dec3fb9b77ba721481f902d09e3f99a3ac2283d794764e6c8ef03eb
|