Batu Lab NotesPractical developer guides

Avoid caching a function that receives mutable input

By Batu ยท English technical notes

Also published in our Blogger archive.

Avoid caching a function that receives mutable input

A list is mutable and unhashable, so decorating a function that accepts prices: list[int] with functools.cache fails before the function body runs. More importantly, a cache key should describe the values used for the calculation, not a list object that a caller can append to later.

This recipe leaves total_after_discount uncached. It immediately creates tuple(prices) and passes that snapshot to the private cached function. The first list contains 10 and 20, producing 27.00 after the ten-percent discount. After prices.append(30), the boundary function makes a different tuple, (10, 20, 30), and the result is 54.00. The assertions distinguish the old cached calculation from the new input state.

The snapshot is shallow: a tuple containing mutable or unhashable elements is not a safe key for cache, and later changes inside those elements can still invalidate the meaning of a result. Converting an iterator also consumes it, which may be surprising for a one-shot generator. Choose a different immutable representation when order is irrelevant or when values need normalization. The cached helper must remain a pure calculation; cached calls can otherwise suppress required side effects.

from functools import cache


@cache
def _discounted_total(price_snapshot):
    return sum(price_snapshot) * 0.90


def total_after_discount(prices):
    return _discounted_total(tuple(prices))


prices = [10, 20]
before_mutation = total_after_discount(prices)
prices.append(30)
after_mutation = total_after_discount(prices)

assert before_mutation == 27.0
assert after_mutation == 54.0
assert _discounted_total((10, 20)) == before_mutation
print(f"before mutation: {before_mutation:.2f}")
print(f"after mutation: {after_mutation:.2f}")

Expected stdout:

before mutation: 27.00
after mutation: 54.00

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

Source: Python functools.cache documentation.