Changelog
Notable changes for people using this library. Format follows Keep a Changelog; versions follow Semantic Versioning.
From v0.7.0 on, what may and may not change is fixed by six principles — the first being that every version reads every value an earlier version wrote and accepts every call v0.6.0 or later accepted. The breaks listed under v0.1.0–v0.6.0 predate that promise.
That promise points one way only: a new version reads what an old one wrote, not the other way round. Where several processes share a cache and are upgraded one at a time, check the release's Upgrading notes before rolling — the older ones are the readers at risk.
[0.7.1] - Unreleased
Fixed
- A value that cannot be decompressed is reported instead of returned
undecoded. A reader without
zstandardcannot decompress what a writer that had it produced, and the resulting error was being treated as "this is not the v0.7.0 format after all", sending the value down the pre-0.7.0 read path. For the two types that path accepts anything from —bytes, where every byte string is valid, andstr, through the pre-0.3.0 bare-string reader — the rawdata:...;base64,...envelope came back as the value: aCachetic[bytes]returned it in silence, and aCachetic[str]with a single log line. Both now raiseDecompressionError, as every other value type already did. AffectsCacheticandAsyncCacheticalike; installcachetic[zstd]on every process that shares a cache, or on none. - The same conflation applied to validation: a payload that decoded but failed
its type's validation was retried as legacy data. It now raises. The only
values that read differently are v0.2.0 strings shaped like one of Cachetic's
own Data URLs whose payload is valid base64 and invalid JSON, which
_loads_bare_stralready documents as unrecoverable.
Detection of the format itself is unchanged: a value carrying one of Cachetic's
headers whose envelope does not decode still falls back to the legacy path, so
a bytes cache that stored a real data URI keeps reading it.
[0.7.0] - 2026-07-28
Added
AsyncCachetic, an async client mirroringCacheticmethod for method on all four backends. Both write byte-identical values, so one cache can be read by a sync worker and an async web app at once, and a call site can be migrated one at a time.exists(key)on both clients — answers whether a key is present without fetching and deserialising the value.cachetic.close_all()andawait cachetic.aio.close_all()to release shared backend clients. Safe to call more than once; a cache used afterwards reconnects rather than failing.- PostgreSQL backend, via
pip install cachetic[postgres]. Table name and pool size come from the URL:?table=,?pool_min_size=,?pool_max_size=. gettakes a realdefault:cache.get("missing", "fallback")returns"fallback". Earlier versions accepted the argument and discarded it.cache_urlaccepts a client you built yourself — aredis.Redisordiskcache.Cache, or aredis.asyncio.RedisforAsyncCachetic. It is used as-is, never entered into the shared registry, and not closed byclose_all(): closing a pool this library did not open would break it for whatever else holds it. MongoDB and PostgreSQL stay URL-only, because their backends also need?collection=/?table=, which a bare client cannot carry.
Changed
- BREAKING —
redisis no longer installed by default. It moved from a core dependency to thecachetic[redis]extra, alongsidecachetic[mongodb],cachetic[postgres]andcachetic[zstd]. Only the disk backend ships in the base install. Add the extra if you use aredis://URL. - BREAKING — environment variables need the
CACHETIC_prefix.CACHE_URLbecomesCACHETIC_CACHE_URL, and likewise forDEFAULT_TTL,PREFIXandCOMPRESSION. Earlier versions read the bare names despite the documentation saying otherwise, which also let a genericPREFIXleak in from an unrelated part of the environment. - BREAKING —
default_ttl=0now turns the whole client off. Reads miss,existsreportsFalse, and writes are dropped whateverexan individual call carries. It used to drop writes only, so a client configured to disable caching kept serving whatever an earlier client had written. It still does not evict: values another client wrote stay where they are, and a per-callex=0is unchanged — it skips that one write and leaves any existing entry alone. - BREAKING —
.cachereturns an adapter, not the driver. It used to hand back the realredis.Redis/diskcache.Cache, socache.cache.scan_iter(...)worked; it now returns a four-methodCacheProtocol. This is deliberately not restored — the adapter is what lets every backend answer the same call the same way. To reach the driver's own methods, build the client yourself and pass it ascache_url. - BREAKING —
get_or_raiseno longer raises on a storedNone. For a cache whose type includesNone, a key holdingNoneis a hit. It used to be indistinguishable from a miss, soget_or_raiseraised on keysexistsreported as present. - BREAKING — zstd compression is now an extra.
pip install cachetic[zstd]rather than a side effect of havingzstandardimportable. Every process sharing a cache must agree: a writer that has it prefers zstd, and a reader without it cannot decompress the result. - BREAKING — the backend adapter protocol is positional-only.
MongoCachein particular no longer acceptsset(name=..., value=...). Affects only code calling an adapter directly rather than throughCachetic. - Values are stored as a self-describing Data URL —
data:application/json;compression=zstd;base64,<payload>— carrying the compression algorithm with the value, so a reader never needs to know the writer's settings. Values written by earlier versions are still read; no migration is needed. - Backend clients are shared per URL. Constructing a
Cacheticopens nothing, reaching for.cacheopens nothing, and the first operation opens exactly one client that every other instance on that URL reuses — including the pools and monitor threads the driver keeps. Nothing is cached in your process, so a value another client deleted is gone here too. - The PostgreSQL backend no longer uses peewee. Both clients talk to psycopg
directly, so the extra no longer pulls in an ORM, the whole URL reaches psycopg
(
sslmodeand friends now work on the sync client too), and the pool keeps one connection warm instead of four — a cache is optional infrastructure, and four becomes thirty-two across eight workers. - Extra arguments are accepted again.
get,set,deleteandget_or_raisetake stray positional and keyword arguments as they did in v0.6.0 and still ignore them, now with aDeprecationWarningnaming what was dropped instead of discarding it in silence.existsis new in this release and takes exactly its documented arguments. richandstr_or_noneare no longer dependencies, and thepydanticpin relaxed from>=2,<3to>=2.
Fixed
import cacheticno longer requiresrich. v0.6.0 droppedrichfrom its install requirements while still importing it at module scope, sopip install cachetic==0.6.0followed byimport cacheticraisedModuleNotFoundErrorin an environment that had nothing else pulling it in.- Values written by v0.2.0 with
object_type=strare readable again. They were stored as bare UTF-8 before the format became JSON in v0.3.0. Note the cost, which cannot be avoided: under that format every byte string is a valid value, so aCachetic[str]can no longer tell a truncated write from a real v0.2.0 string, and logs a warning rather than raising. Other value types are unaffected. Values pickled by v0.2.0 underobject_type=objectremain deliberately unreadable — deserialising them executes arbitrary code. - Lazy expiry no longer discards a concurrent write. MongoDB and PostgreSQL
delete an entry when a read finds it expired; that delete now matches on the
deadline it just read, so a
setlanding in between is not clobbered. Redis and diskcache enforce their own deadlines and were never affected. - zstd compression is safe to use from several threads.
zstandard's one-shot API reuses an internal context, and one shared instance could crash the interpreter — Cachetic clients are designed to be shared. Contexts are now per thread. await cachetic.aio.close_all()releases disk handles too. They live in the synchronous registry, becausediskcachehas no async API and no event loop of its own; without this an application that only ever awaited the async teardown never closed a single SQLite handle.- A lost
CREATE TABLE IF NOT EXISTSrace is no longer fatal. A fleet starting at once against an empty PostgreSQL database used to have one instance fail to start.
Upgrading
Go through v0.6.1 first if more than one process shares the cache. Principle
1 promises that a new version reads what an old one wrote; it says nothing about
the reverse, and v0.6.0 cannot read the Data URL format described above. Any
rolling deployment runs both versions against the cache at once, so upgrading
straight from v0.6.0 leaves the pods that have not restarted yet unable to read
anything a v0.7.0 pod has written — a ValidationError for most types, and the
undecoded envelope returned as the value for a Cachetic[bytes], which no
caller-side try/except catches. With the default default_ttl=-1 those
values never expire, so rolling back does not clear them.
v0.6.1 reads this format and still writes the old one, which makes both hops safe to have half-deployed and safe to roll back. Deploy it everywhere, then move to v0.7.0. A process that is the only one on its cache, or one that can be stopped entirely before the new version starts, can upgrade directly.
bytes caches are the one case needing attention: a value written by v0.5.x or
v0.6.x with compression=True carries no algorithm marker, and every byte string
is a valid bytes, so there is nothing to detect. Keep compression=True on that
client to read its own old data. Values written from v0.7.0 on say which algorithm
they used and read back under either setting.
Everything else written by v0.1.0 onwards is read without migration, with the v0.2.0 pickle exception noted above. See Upgrading to v0.7.0 for the details.
[0.6.1] - 2026-07-28
Maintained on the v0.6.x
branch, not on main. It exists only to make the upgrade to v0.7.0 safe on a
shared cache; nothing else is backported to it.
Added
- Reads the value format v0.7.0 writes, without writing it. A v0.6.1 process is compatible with v0.6.0 and v0.7.0 at the same time, which is what makes it safe to be halfway through rolling either way. See the Upgrading note under v0.7.0 for why going straight from v0.6.0 is not.
cachetic[zstd]extra, needed to read what a v0.7.0 writer that has it produced. Without the library such a read raisesDecompressionErrorrather than returning the undecoded value.
Changed
- Compressed writes are always zlib, where v0.6.0 used zstd whenever
zstandardhappened to be importable. That made the stored algorithm a property of the image rather than of the data, and thezstdextra above is exactly the sort of thing that changes an image. zlib is in the standard library, so a v0.6.1 write stays readable by the v0.6.0 processes it is rolling over whatever either of them has installed.
Fixed
__version__reported0.4.1on v0.6.0.
[0.6.0] - 2026-03-18
Added
- MongoDB clients are pooled: instances pointing at the same URL share one
MongoClient, and the unique index is created once per database and collection rather than on every instantiation.
Fixed
- Debug log lines for
get/setare built only when debug logging is on.
Known issue
richwas removed from the install requirements but is still imported at module scope, soimport cacheticfails in an environment that does not otherwise have it. Fixed in v0.7.0; installingrichalongside works around it.
[0.5.0] - 2025-12-29
Added
compressionoption. Values are compressed before storage — zstd whenzstandardis importable, zlib otherwise — and decompressed on read, with the format detected from the data.- A published documentation site.
Changed
- The
pydantic-settingsversion pin was relaxed.
[0.4.1] - 2025-08-20
Changed
- BREAKING — the MongoDB extra was renamed from
cachetic[mongo]tocachetic[mongodb]. Installs naming the old one fail. richbecame a runtime dependency, used to pretty-print cache keys in debug logs.
[0.4.0] - 2025-07-24
Added
- MongoDB backend. Pass a
mongodb://URL with a database path and a?collection=parameter, and installcachetic[mongo]. delete(key).
[0.3.0] - 2025-07-15
Changed
- BREAKING —
object_typeis now apydantic.TypeAdapter.object_type=Personbecomesobject_type=pydantic.TypeAdapter(Person). - BREAKING — everything is stored as JSON via the adapter, except
bytes, which is still stored raw. This replaces the per-type encodings v0.2.0 used. - BREAKING — values written by v0.2.0 for
object_type=object(pickle) andobject_type=str(bare UTF-8) can no longer be read. Those caches have to be repopulated. v0.7.0 restores thestrhalf. - BREAKING —
cache_prefixwas renamed toprefix,cache_ttltodefault_ttl, andcache_urlbecame required. cache_urlalso accepts an already-builtredis.Redisordiskcache.Cache.
Removed
- BREAKING — caching arbitrary objects via
pickle(object_type=object).
[0.2.0] - 2025-04-21
Added
object_typeacceptsbytes,str,int,float,bool,list,dict, apydantic.BaseModelsubclass, aTypeAdapter, orobjectfor anything picklable. Each type has its own wire format —str, for instance, is stored as bare UTF-8 rather than JSON.- The connection is checked on first use — a Redis
PING, or a trial write for the disk cache — raising instead of failing later. get_cache_key(), returning the effective prefixed key.
Changed
- BREAKING —
cache_urlandcache_dirmerged intocache_url. Pass aredis://URL or a filesystem path;cache_diris gone. - BREAKING — the default
object_typebecameobject, so an unparameterisedCachetic()pickles values rather than validating them as a model. - The
diskcacheandredispins dropped their upper bound.
Removed
- BREAKING —
get_objects(),get_objects_or_raise()andset_objects(), with no replacement.
[0.1.0] - 2025-02-23
Initial release. Cachetic[T] caches a Pydantic model type to a local
diskcache directory or a Redis server, with get / get_or_raise / set for
single objects, get_objects / get_objects_or_raise / set_objects for lists,
and TTL and key-prefix support. Values are serialised with model_dump_json().