cache-results
A Django decorator that caches a function's results by its arguments, in any Django cache, such as Redis.
from cache_results import cache_results
@cache_results(timeout=600)
def get_book(book_id: int) -> dict:
return Book.objects.values().get(pk=book_id)
get_book(42) # computed and cached
get_book(42) # served from the cache
get_book.invalidate(42) # clears that entry
Documentation: https://docs.cache-results.smarter.sh/
cache-results grew out of the caching layer of Smarter, where it caches database reads and third-party api calls across many application servers.
Features
- Safe keys. The key is the function's module and qualified name, plus a
hash of its arguments bound to its signature, so
f(1),f(a=1)andf(1, b=2)(whenbdefaults to 2) share one entry. Arguments that can't be part of a safe key, such as model instances, run the function uncached and log an error, rather than be keyed bystr(). Noneis cached like any other result.- **
.invalidate(\*args, **kwargs)** on every decorated function clears the entry of those arguments. - async functions are cached without blocking the event loop.
- Classmethods are cached per class. Instance methods are refused, because
selfwould be part of the key. - No stampede within a process. Threads that miss the same key at once wait for the first one to compute it.
- Safe during startup. A function called before Django's apps are ready runs uncached, rather than initialize the cache framework too early.
lazy_cache, Django's cache imported only once Django is ready, for rawget()andset().- Configurable through one Django setting,
CACHE_RESULTS. - Depends only on Django and asgiref.
Installation
Install it from PyPI:
pip install cache-results
It needs Python 3.10 or newer and Django 4.2 or newer, and works with any Django cache backend. With a shared cache such as Redis, cached results survive restarts and are shared by every application server.
Arguments
The arguments are the cache key, so they must be plain values: str, int,
float, bool, None, and lists, tuples, dicts and sets of them, plus
datetimes, Decimals, UUIDs, classes and dataclasses. Pass a model's id, not the
model:
@cache_results()
def get_profile(user_id: int) -> Profile:
return Profile.objects.select_related("user").get(user_id=user_id)
get_profile(request.user.pk)
To allow another type, subclass CacheKeyEncoder and name your subclass in
the KEY_ENCODER setting:
from cache_results import CacheKeyEncoder
class MyCacheKeyEncoder(CacheKeyEncoder):
def default(self, o):
if isinstance(o, Money):
return {"__money__": [str(o.amount), o.currency]}
return super().default(o)
Never pass a secret, such as an api key, as an argument: arguments are part of the key, and verbose logging writes them to the log. Pass the id of the record that holds it, and read the secret inside the function.
Functions defined inside an inherited classmethod
A qualified name records where a function is defined, not who calls it. A function defined inside a classmethod that subclasses inherit has the same qualified name for every subclass, so subclasses would share its entries. Give it the class name as a parameter:
class Resource(models.Model):
class Meta:
abstract = True
@classmethod
def get_cached(cls, pk: int):
@cache_results()
def _get(pk: int, class_name: str = cls.__name__):
return cls.objects.get(pk=pk)
return _get(pk)
Settings
Every entry of the CACHE_RESULTS setting is optional:
CACHE_RESULTS = {
# the CACHES alias that holds cached results.
"CACHE_ALIAS": "default",
# the default timeout, in seconds, of a decorated function that names none.
# Unset, it is the cache alias's own TIMEOUT.
"TIMEOUT": 3600,
# text that starts every key. Changing it, or VERSION, abandons every cached
# result at once, without touching anything else in the same cache.
"KEY_PREFIX": "",
"VERSION": None,
# a subclass of cache_results.CacheKeyEncoder, to allow more argument types.
"KEY_ENCODER": "cache_results.keys.CacheKeyEncoder",
# zero-argument callables that return whether to log cache hits, misses and
# invalidations, and whether those logs include the arguments.
"LOGGING_ENABLED": None,
"VERBOSE_LOGGING": None,
# a callable(text, event) that formats the start of each log message, where
# event is "hit", "miss", "invalidate" or "".
"LOG_PREFIX": None,
# with LOG_PREFIX = "cache_results.colors.log_prefix", which colors log
# messages by event: a dict from event to ANSI code.
"LOG_COLORS": None,
}
Callables and classes are given as dotted paths or as the objects themselves.
Settings are read on every call, so override_settings works in tests.
lazy_cache
For raw get() and set(), use lazy_cache in place of
django.core.cache.cache. It has the same api, and imports Django's cache only
on first use, once Django's apps are ready: importing it while Django is
starting can leave it on a local-memory backend that persists nothing.
from cache_results import lazy_cache as cache
cache.set("my_key", "my_value", timeout=300)
cache.get("my_key") # "my_value"
On first use it checks the cache, and warns when the default cache is a
local-memory or dummy backend.
Decorator options
@cache_results(timeout=None, cache_key=None, logging_enabled=False)
timeout: seconds.Noneuses theTIMEOUTsetting, and0caches nothing.cache_key: a fixed key that every call shares, whatever its arguments. Use it only for functions whose result doesn't depend on them.logging_enabled: log this function's hits, misses and invalidations even when theLOGGING_ENABLEDsetting is off. Logs name the function and the type of its result, never the result.
Development
make # list every command
make init # create venv/ from requirements/local.txt, install the package and the git hooks
make test # run the tests against the local-memory cache
make test-redis # run the tests against Redis
make coverage # run the tests with a coverage report
make lint # run the pre-commit hooks on every file
make typecheck # check the type annotations with mypy
make sphinx-build # build the Sphinx docs into docs/build/html
make sphinx-publish # publish them to docs.cache-results.smarter.sh
make build # build the sdist and wheel, and check them
make python-requirements # recompile the pinned requirements/*.txt
make test-redis uses redis://localhost:6379/15 unless you set
REDIS_URL, and clears that database. Running pytest yourself, set
CACHE_RESULTS_TEST_REDIS_URL instead.
CI runs the hooks, the tests on every supported pair of Python and Django
versions and against Redis, and a build, on every pull request, and on every
push to main, alpha and beta as the first step of the Release workflow.
Coverage is reported to Codecov.
When CI passes, the Release workflow releases with semantic-release, from the
conventional commit messages, and publishes to PyPI from main (or to TestPyPI
from alpha and beta). make release-dry-run shows the next version.
Contributing
Bug reports, questions and pull requests are welcome. Please open an issue before starting on a larger change, so that we can agree on its design first.
- Fork the repository, and branch from
main. - Run
make init. It also installs the pre-commit hooks, which are the lint standard (black, flake8, isort, pyupgrade, codespell, bandit, Prettier) and check each commit message. - Make your change with its tests.
make coveragemust stay at 95% or more, and bothmake testandmake test-redismust pass. CI checks all of this on your pull request. - Write conventional commits, e.g.
fix(keys): sort frozensets of tuples. Releases are versioned from them:fixmakes a patch release,feata minor one. - Open a pull request that says what changed and why, and links its issue.
A few rules keep @cache_results() safe and readable:
- The package depends only on Django and asgiref. Project-specific behavior
belongs in a project's own hooks, named by the
CACHE_RESULTSsetting. - Arguments are never keyed by
str(), and results and secrets are never logged. - A change to the key format, or to the value stored for
None, makes every cached entry unreadable, so it needs a major release.
Governance of the Smarter projects is described in smarter-sh/community. Everyone who takes part is expected to follow the code of conduct.
License
Metadata
Release files for cache-results 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| cache_results-0.2.0.tar.gz | 30.7 kB | With provenance | Details for cache_results-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| cache_results-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for cache_results-0.2.0-py3-none-any.whl |
Total release size: 54.6 kB
Release files / cache_results-0.2.0.tar.gz
| Download URL | cache_results-0.2.0.tar.gz |
|---|---|
| Size | 30.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2928789efa390cf4bea33b182801de886647afdb1e394023eb50cf5369ab37f8
|
|
BLAKE2b-256 checksum How to use checksums |
c374e1c9571d5c2af16e6ca19aa719ab481a990cf71867a14d44ca4bfc6da58b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logRelease files / cache_results-0.2.0-py3-none-any.whl
| Download URL | cache_results-0.2.0-py3-none-any.whl |
|---|---|
| Size | 24.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
581c7b09d212e9b9a82109f5ac5d194c4715ec449e28f9750f6092755ddafb81
|
|
BLAKE2b-256 checksum How to use checksums |
a9a16afb1b4b94da287973ae57ef69945c87072fb1b38807a2fcc755fc4f5e2a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logSecurity for cache-results 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for cache_results-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.