Avoid caching a function that receives mutable input
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.