Back to Blog
Python

Python functools lru_cache: Caching Function Results

Learn how to use python functools lru_cache to memoize function results, manage cache size with maxsize, and avoid common pitfalls in real applications.

lru_cachefunctoolsmemoizationcachingperformance optimization
Illustration of a Python function wrapped by an LRU cache decorator showing cached results being reused for repeated calls.

python functools lru_cache is a decorator from the standard library's functools module that memoizes function results. When a decorated function is called with arguments it has already seen, the cached return value is returned immediately instead of re-executing the function body.

Basic Usage and Syntax

Applying lru_cache is a one-line change. The decorator wraps the function in a callable that maintains a dictionary mapping call arguments to return values. On a cache hit, the stored value is returned; on a miss, the function executes and the result is stored.

from functools import lru_cache @lru_cache(maxsize=128) def fibonacci(n): if n < 2: return n return fibonacci(n - 1) + fibonacci(n - 2)

The maxsize parameter controls how many results are retained. When the cache exceeds this limit, the least recently used entry is evicted. Setting maxsize=None creates an unbounded cache that never evicts entries.

How the Cache Key Is Derived

The cache key is built from the positional and keyword arguments passed to the function. Each argument must be hashable; if an unhashable argument such as a list or dictionary is passed, the call raises a TypeError rather than falling back to executing the function.

@lru_cache(maxsize=None) def process(items): return sum(items) process([1, 2, 3]) # TypeError: unhashable type: 'list'

This constraint follows from the underlying implementation: the cache is a dictionary, and the key must be hashable. If you need to cache functions that receive mutable containers, convert them to hashable forms before calling, such as tuples for lists or frozenset for sets.

maxsize and typed Parameters

The maxsize parameter accepts an integer or None. A maxsize of 0 disables caching entirely while still applying the LRU wrapper, which is rarely useful. A maxsize of 1 keeps only the most recent result.

The typed parameter, when set to True, treats arguments of different types as distinct even when they compare equal. For example, 1 and 1.0 would be cached separately:

@lru_cache(maxsize=128, typed=True) def normalize(value): return value * 2

With typed=False (the default), 1 and 1.0 hash to the same key and share a cache entry. This matters when the function's behavior depends on the exact type of its arguments, such as functions that perform type-specific formatting or serialization.

LRU Eviction Behavior

LRU stands for least recently used. Each cache entry tracks when it was last accessed. When the cache is full and a new entry must be inserted, the entry that has not been accessed for the longest time is removed.

This eviction policy is a tradeoff: it keeps frequently accessed results available while bounding memory usage. For workloads with a small working set, maxsize=None is often simpler, but for long-running processes where argument combinations are numerous or unpredictable, a bounded cache prevents unbounded memory growth.

The eviction order is observable through cache_info():

fibonacci.cache_info() # CacheInfo(hits=..., misses=..., maxsize=128, currsize=...)

The hits and misses counters provide insight into how effectively the cache is being used. A low hit rate suggests the cache size is too small or the argument space is too large.

Cache Management Methods

Every lru_cache-decorated function exposes three methods:

  • cache_info() returns a CacheInfo named tuple with hit and miss counts, the configured maxsize, and the current size.
  • cache_clear() removes all cached entries and resets the hit and miss counters.
  • cache_parameters() returns a dictionary of the parameters the decorator was called with, including maxsize and typed.
fibonacci.cache_clear() fibonacci.cache_info() # CacheInfo(hits=0, misses=0, maxsize=128, currsize=0)

cache_clear() is useful when the underlying data the function depends on changes, even though the function signature stays the same. For example, if a cached function reads from a configuration object that is updated at runtime, stale results can be invalidated by clearing the cache.

When Caching Is Appropriate

lru_cache is only safe for pure functions: functions whose return value depends solely on their arguments and that have no side effects. If a function reads from external state, writes to a file, sends a network request, or mutates a global object, caching can produce incorrect results.

A common pattern is caching the result of an expensive computation that is repeated with the same inputs, such as parsing a large configuration file, computing a hash, or normalizing text. The cache reduces CPU work at the cost of memory.

Functions that return mutable objects are also risky. If a caller mutates the returned list or dictionary, the cached value changes for every subsequent caller. Returning a copy from the cached function, or ensuring callers do not mutate results, avoids this class of bugs.

Performance and Memory Considerations

The cache adds a dictionary lookup and an LRU bookkeeping step to every call. For functions that execute quickly, the overhead of the cache can exceed the cost of simply running the function. lru_cache is most beneficial when the function body is computationally expensive relative to the lookup overhead.

Memory usage grows with the number of distinct argument combinations. Each entry stores the arguments and the return value. For functions that return large objects, an unbounded cache can consume significant memory. A bounded maxsize caps this growth but introduces eviction, which can reduce hit rates if the working set is larger than the cache.

The typed=True option doubles the effective key space when numeric types are mixed, which can reduce hit rates for functions that receive both integers and floats.

Alternatives to lru_cache

Python 3.9 introduced functools.cache, a simpler unbounded memoization decorator:

from functools import cache @cache def expensive(x): return x * x

functools.cache is equivalent to lru_cache(maxsize=None) but with less overhead because it skips the LRU bookkeeping. Use it when you know the argument space is small enough that unbounded caching is safe.

For methods that depend on instance state, functools.cached_property caches a computed attribute on the instance rather than on a global cache. This avoids the common pitfall of applying lru_cache to a method and accidentally sharing results across instances.

For more complex eviction policies, such as time-based expiration, a custom cache or a third-party library is needed. lru_cache has no built-in TTL mechanism; entries only expire through eviction or explicit cache_clear() calls.

python functools lru_cache: Practical Usage and Code Example | RYUSLOG DEV