Batu Lab NotesPractical developer guides

Make an immutable configuration key for caching

By Batu ยท English technical notes

Also published in our Blogger archive.

Make an immutable configuration key for caching

A cache key should capture the configuration values that affect a result in a hashable form. RenderKey is a frozen dataclass with a template string, an integer width, and a tuple of (name, value) style pairs. make_render_key receives a mutable style dictionary but sorts its items and stores the resulting tuple. Thus the insertion order of the input mapping cannot create different keys for the same style settings.

The example starts with {"theme": "dark", "accent": "cyan"} and produces dark/cyan:release:8. It then mutates the original dictionary to "light"; the first RenderKey still contains the earlier dark theme. An independently constructed equivalent key compares equal, so the second render_label call is a cache hit. cache_info() reports one hit and one miss for this controlled sequence.

frozen=True prevents rebinding fields on the dataclass instance, but it does not deep-freeze arbitrary field contents. Do not put a list, dictionary, or mutable custom object inside a key and assume it is safe or hashable. Sorting also requires style keys to be mutually orderable; for heterogeneous keys, define a canonical serialization or reject them. Finally, clear or bound a cache if configuration combinations can grow without limit.

from dataclasses import dataclass
from functools import cache


@dataclass(frozen=True)
class RenderKey:
    template: str
    width: int
    style: tuple[tuple[str, str], ...]


def make_render_key(template, width, style):
    if width <= 0:
        raise ValueError("width must be positive")
    return RenderKey(template, width, tuple(sorted(style.items())))


@cache
def render_label(key):
    settings = dict(key.style)
    return f"{settings['theme']}/{settings['accent']}:{key.template}:{key.width}"


style = {"theme": "dark", "accent": "cyan"}
key = make_render_key("release", 8, style)
style["theme"] = "light"
equivalent_key = make_render_key(
    "release", 8, {"accent": "cyan", "theme": "dark"}
)

first = render_label(key)
second = render_label(equivalent_key)
info = render_label.cache_info()
assert key.style == (("accent", "cyan"), ("theme", "dark"))
assert first == second == "dark/cyan:release:8"
assert (info.hits, info.misses) == (1, 1)
print(first)
print(f"cache: {info.hits} hit, {info.misses} miss")

Expected stdout:

dark/cyan:release:8
cache: 1 hit, 1 miss

By Batu. AI assistance was used to prepare this article.

Source: Python dataclasses.dataclass documentation.