Skip to main content

DialCache for Python

An asyncio port of DialCache for Python 3.11 and later. Each use case declares its identity and policy; an enabled request scope opts into request memoization, local storage, Redis, and concurrent request sharing. The behavioral contract is the repository's portable specification.

Once the first PyPI release is available, install it with:

python3 -m pip install 'dialcache[redis]'

Until that release is published, install from the root of a repository checkout:

python3 -m pip install './python[redis]'

For local-only caching, omit the redis extra. Zstandard is included for the portable Redis payload format. The application owns the asyncio event loop and any Redis client connections.

A cached function

Select cache identity explicitly with cache_key. This API excerpt assumes an application database:

from dialcache import DialCache, Policy

cache = DialCache(namespace="my-service")


@cache.cached(
    use_case="user-profile",
    key_type="user",
    cache_key=lambda user_id, locale: {"id": user_id, "args": {"locale": locale}},
    default_config=Policy(ttl_sec={"local": 5}, request_local=True),
)
async def get_profile(user_id: str, locale: str) -> dict:
    return await database.fetch_profile(user_id, locale)


async def handle_request(user_id: str, locale: str) -> dict:
    async with cache.enable():
        first = await get_profile(user_id, locale)
        second = await get_profile(user_id, locale)  # Same request memo.
        return second

The selector returns an entity ID, or {"id": ..., "args": {...}} when the result has additional dimensions. Here, id groups the user's tracked values for invalidation; args distinguishes locales within this use case. Include every input that affects the result, including tenant or authorization scope when relevant. Explicit use_case and argument names keep identity stable across function refactors and languages.

The selector receives the same positional and keyword arguments as the loader. Match their parameter names and repeat any defaults the loader accepts; loader defaults are not filled in before calling the selector. For inferred identity, id_arg, arg_adapters, and ignore_args remain supported alternatives; see the Python identity guide.

Calls outside enable() go directly to the source. They do not construct cache keys, resolve policy, share concurrent work, or apply a DialCache source deadline. Both with cache.enable(): and async with cache.enable(): are valid; the wrapped function is always awaitable. Synchronous loaders are accepted and run on the event loop, so use async loaders for blocking I/O.

Nested enabled scopes share the live outer request memo. A nested cache.disable() temporarily bypasses caching without deleting that memo. Closing the outer scope clears the memo and prevents late publication. Async tasks that inherited a scope use pass-through behavior for calls made after that scope closes. Cache instances keep independent contexts.

Policies and runtime changes

No layer is enabled by default. A positive TTL enables that shared layer, with a default rollout percentage of 100. Request memoization defaults to false; concurrent same-key sharing defaults to true.

policy = Policy(
    ttl_sec={"local": 5, "remote": 60},
    ramp={"remote": 25},
    request_local=True,
    coalesce=True,
    remote_read_timeout_ms=50,
    stale_on_error_max_age_sec=120,
)

TTLs are integer seconds from 1 through 31,536,000. Rollout percentages are finite numbers from 0 through 100. Sampling is stable per exact key and layer, using the same cohort algorithm as the TypeScript, Go, and Rust ports.

Pass a synchronous or asynchronous policy_provider to DialCache to resolve runtime settings once per enabled invocation. It receives the structured key and returns a Policy, a mapping, or None:

async def policy_provider(key):
    if key.use_case == "user-profile":
        return {"ramp": {"remote": 50}}
    return None


cache = DialCache(policy_provider=policy_provider)

Runtime replies are sparse: an omitted field inherits the operation default. A whole reply of None inherits the complete operation policy. An explicit None leaf is malformed and cannot silently inherit a valid setting. Python snake_case names and the shared corpus's camelCase mapping names are accepted. Policy objects snapshot their input maps so later mutation cannot alter an already admitted invocation.

Policy.disabled() explicitly disables inherited request memoization, local and remote serving, recovery, and shadow work. It does not cancel work that was already admitted or disable explicit invalidation. Policy.enabled(ttl) enables local and remote TTLs; it does not opt into request memoization.

Invalid static defaults raise ConfigError at registration. At runtime, invalid TTLs or ramps disable their own layer; malformed boolean switches, read deadlines, containers, or provider failures bypass caching for that enabled invocation. Optional recovery and shadow failures leave ordinary serving available.

Redis and tracked invalidation

from redis.asyncio import Redis
from dialcache import DialCache, Policy
from dialcache.redis import RedisAdapter

client = Redis.from_url(
    "redis://localhost:6379",
    decode_responses=False,
    socket_connect_timeout=0.5,
    socket_timeout=0.5,
)
cache = DialCache(redis=RedisAdapter(client))


@cache.cached(
    use_case="user-profile",
    key_type="user",
    cache_key=lambda user_id: user_id,
    track_for_invalidation=True,
    default_config=Policy(ttl_sec={"remote": 60}),
)
async def get_profile(user_id):
    return await database.fetch_profile(user_id)


async def update_profile(user_id, changes):
    await database.update_profile(user_id, changes)
    await cache.invalidate_remote("user", user_id)

The adapter borrows a redis.asyncio.Redis or RedisCluster client; close it with await client.aclose() when your application shuts down. Configure finite connection, socket, and retry budgets on the client. Tracked reads atomically read the value and watermark from a primary. For tracked Cluster reads, use a dedicated client constructed with primary-only defaults: read_from_replicas=False, load_balancing_strategy=None where supported, and no custom connection hook. Keep its configuration and connection mode unchanged while borrowed. Do not repurpose a previously READONLY pool by resetting flags; create a new primary-only client. Unsafe tracked reads raise RedisProtocolError at the adapter boundary and ordinary cache calls fail open to the source. Replica-enabled clients remain usable for untracked reads and maintenance. Keys for one tracked entity share a Redis Cluster hash tag.

Each write stores a complete version-1 frame using one native SET. A tracked frame is readable only if its writer timestamp is strictly greater than the invalidation watermark. Value writes never create or extend watermarks. Tracked physical value TTLs are capped at one hour. Invalidation raises on mutation failure; ordinary cache plumbing fails open to the source.

Local storage is process-local. Remote invalidation does not synchronously clear already warmed local entries or request memos on any instance. Choose local TTLs with that explicit consistency limit in mind.

Deadlines, recovery, and observability

The default source deadline is 60,000 ms for enabled calls. The default Redis read deadline is 50 ms and can be overridden by operation or runtime policy. Deadline budgets are integer milliseconds from 1 through 2,147,483,647; an explicit fallback_timeout_ms=None disables the source deadline. Timing uses the monotonic clock, while Redis frames and invalidation use wall time.

Deadline expiration stops the caller's wait. It cannot retract a source operation or a Redis command that already started. Late results cannot publish through an expired source execution. Caller cancellation likewise must not cancel another caller's shared execution.

Stale recovery is optional and requires a maximum age strictly greater than the remote TTL. A valid candidate is retained from the original remote read; an eligible source rejection can use it only before the exclusive maximum age. The default recovery predicate admits DialCache's own FallbackTimeoutError. Recovered values may memoize in still-open request scopes; recovery does not refresh Redis or local storage.

Pass a synchronous metrics callback or an object with observe(event) to receive the backend-neutral diagnostic event dictionaries. Their label names match the shared contract, including cacheNamespace, useCase, keyType, and layer. Observer failures do not alter cache results. Local capacity defaults to 10,000 entries; zero capacity disables storage while preserving eligible concurrent sharing.

Relationship to gcache

The Python API takes inspiration from Galileo gcache: decorated functions, argument-based identity, explicit context managers, and pluggable serializers. DialCache follows its own portable specification for behavior and wire compatibility. The API design notes record the source-reviewed gcache revision and the native API choices made for this port.

This binding exposes awaitable operations. It does not introduce a global singleton, implicitly run synchronous I/O in a thread pool, serialize with pickle, take ownership of Redis connections, or change the rollout cohort randomly. Direct put, delete, and flush cache APIs from gcache are outside DialCache's portable contract; writes come from successful source loads and entity-level invalidation is explicit.

Development and conformance

From the repository root:

python3 -m venv python/.venv
python/.venv/bin/python -m pip install -e './python[test,redis]'
python/.venv/bin/python -m pytest python/tests

The native tests cover Python API behavior, policy validation, scope lifetime, local expiry, cancellation, and wire boundaries. Shared replay runs the real Python API through the repository's Node coordinator. Its inputs and expected observations come from the same Quint-generated histories used by the other ports; Node is a development dependency, not a runtime dependency of the Python library. See the porting guide for the completion and settlement requirements and the feature map for portable behavior versus native adapter obligations.

Release files for dialcache 0.26.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for dialcache 0.26.0
File Size Uploaded
dialcache-0.26.0.tar.gz 85.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for dialcache 0.26.0
File Interpreter ABI Platform
dialcache-0.26.0-py3-none-any.whl Python 3 none any Details

Total release size: 121.8 kB

Release files / dialcache-0.26.0.tar.gz

Download URL dialcache-0.26.0.tar.gz
Size 85.5 kB
Tags Source
SHA-256 checksum
How to use checksums
446558e0119c139186a1c649995553b77846dd1e421e491f78b15271d1a01f37
BLAKE2b-256 checksum
How to use checksums
fa1809bdce2dbd5032b7628696d15e41a8b39a568d55f5362f007085b1e6cbb5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Sep 26, 2026.

Transparency log

Release files / dialcache-0.26.0-py3-none-any.whl

Download URL dialcache-0.26.0-py3-none-any.whl
Size 36.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
653c58bc7005b52c4880540c5c8005730eac0e5470bca246525232bf570eadb7
BLAKE2b-256 checksum
How to use checksums
693b508f75b1cfc257995e9a37bd4c0709d244f7b671d6524ad3574377335dbc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Sep 26, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.26.0 This release

2 release files

0.25.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page