Skip to main content

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:

  1. Explicit activation: When you call devstash.activate(), it reads your main script (the entrypoint in sys.argv[0]).
  2. Build an AST: The code is parsed into an Abstract Syntax Tree (AST), a structured representation of your Python source.
  3. Rewrite annotated lines: Function calls marked with # @devstash are rewritten to wrap them with the persistent cache helper.
  4. Compile and exec: The rewritten AST is compiled back into Python bytecode and executed in a fresh __main__ namespace.
  5. Persistent storage: Values are stored on disk using pickle, and automatically restored on subsequent runs.
  6. 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.
  7. 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 # @devstash to 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 in requests.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 as flask run, uvicorn, or gunicorn are 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 with python (or uv 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

devstash-0.1.1.tar.gz (19.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

devstash-0.1.1-py3-none-any.whl (13.2 kB view details)

Uploaded Python 3

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

Hashes for devstash-0.1.1.tar.gz
Algorithm Hash digest
SHA256 21b2579eda41bd44d071b52f746730731e2f07df3ff085c77548683e01077260
MD5 39930e753341f8158dfbd1df70870879
BLAKE2b-256 555be9752905aff444b2823cb934f2deab8c81ede08c7eb04ded817b02d80301

See more details on using hashes here.

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

Hashes for devstash-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 a025f63f8bef5b661aa3ed3973a97cf02ecdd2da10029cb7309cb6d135c87bd8
MD5 02efc3be35bbdf2a8aa881c8e819391d
BLAKE2b-256 31a78dfb0dec3fb9b77ba721481f902d09e3f99a3ac2283d794764e6c8ef03eb

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page