Use a Python frozendict as a Cache Key—Without the List Trap
A frozendict is created without complaint, then lru_cache throws an unhashable-type error when you pass it in. What inside it is still breaking the cache key?

- Python 3.15 adds a built-in frozendict that can be a cache argument when all its keys and values are hashable.
- A nested list remains mutable and prevents the mapping from being hashed.
- A MappingProxyType reflects changes to its source; it serves a different purpose from capturing fixed mapping entries.
Make the settings argument hashable
A settings dictionary can prevent an otherwise cacheable function from using lru_cache: the cache needs hashable arguments, and a regular dict does not qualify. Python 3.15's built-in frozendict offers a mapping-shaped alternative. Its keys and values must be hashable.
For a report header, a tuple of column-name strings preserves column order while meeting that requirement. This illustrative function caches a result derived entirely from its settings:
from functools import lru_cache
@lru_cache(maxsize=128)
def make_header(options):
return options["separator"].join(options["columns"])
options = frozendict(columns=("name", "total"), separator=",")
make_header(options)
make_header(options)
print(make_header.cache_info()) The second sequential call can reuse the first result. cache_info() exposes hits and misses; the example keeps up to 128 recent call results.
Construction can succeed while hashing fails
The outer mapping is fixed, but its values retain their own behavior. PEP 814 specifies a shallow copy, so storing a list does not turn that list into an immutable object.
options = frozendict(columns=["name", "total"])
options["columns"].append("tax") # The list can still change.
hash(options) # TypeError: the list is unhashable. Use value types that match both the data's meaning and the hashability requirement. A tuple of strings works for ordered column names. A tuple containing another list still fails. Also distinguish mapping order from value order: frozendicts preserve insertion order for iteration, but equal key-value pairs compare equal regardless of insertion order. The order inside the column tuple remains significant.
A read-only view follows the original dictionary
MappingProxyType exposes a mapping without allowing writes through the proxy. Changes made to the original dictionary remain visible.
| Requirement | Choice |
|---|---|
| Readers should see changes made by the settings owner. | A mapping proxy provides a live view. |
| Keep the key-value associations captured at creation. | Create a frozendict. |
| Pass settings as a cache argument. | Use a frozendict with hashable keys and values. |
Wrapping an ordinary dict in a proxy does not make it hashable: the proxy delegates hashing to its underlying mapping. Neither approach recursively freezes nested objects.
Check what the function expects to receive
A frozendict is not a dict subclass. A strict isinstance(options, dict) check will reject it. A reader that only requires mapping operations may accept collections.abc.Mapping; a function that mutates its input needs a different design.
Caching must also fit the function's behavior. If every call must perform an external action, equal settings do not justify skipping that action. Start with hashable settings and a function whose earlier result is safe to reuse, then inspect cache hits under the workload that matters.