How functools lru_cache Optimizes Python Functions

This article explains how Python's built-in functools.lru_cache decorator optimizes repetitive, computationally expensive function calls. You will learn the mechanics behind memoization, how the Least Recently Used (LRU) eviction strategy manages memory, how to implement the decorator with practical examples, and the key conditions required for using it effectively.


What is functools.lru_cache?

functools.lru_cache is a decorator in Python's standard library designed to implement memoization. Memoization is an optimization technique where the return values of expensive function calls are stored in memory. When the function is subsequently called with the exact same arguments, the cached result is returned immediately, bypassing redundant execution.

The decorator utilizes an LRU (Least Recently Used) cache algorithm, which ensures that the cache does not grow indefinitely. Once the cache reaches its defined size limit, it automatically discards the items that have not been accessed for the longest period.


How It Works Internally

  1. Argument Hashing: When a decorated function is invoked, lru_cache evaluates the positional and keyword arguments passed to it. It combines these arguments into a hashable key.
  2. Lookup: The decorator checks its internal dictionary to see if the key already exists:
    • Cache Hit: If the key is present, the stored result is returned instantly. The entry is marked as recently used.
    • Cache Miss: If the key is absent, the function executes normally. The resulting value is stored in the cache alongside the key.
  3. Eviction: If adding a new entry causes the cache to exceed its configured maxsize, the least recently accessed key-value pair is evicted.

Because inputs are stored as dictionary keys, all arguments passed to a cached function must be hashable (e.g., integers, strings, tuples). Passing unhashable types like list or dict raises a TypeError.


Performance Comparison: A Practical Example

The impact of lru_cache is most evident in recursive algorithms, such as computing Fibonacci numbers.

Without Caching:

def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

# Computing fibonacci(35) takes several seconds due to O(2^n) time complexity.
fibonacci(35)

In this unoptimized version, the function repeatedly recalculates identical subproblems (e.g., fibonacci(3) is computed millions of times).

With lru_cache:

from functools import lru_cache

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

# Computing fibonacci(35) executes in fractions of a millisecond (O(n) time complexity).
print(fibonacci(35))

By caching previous results, the time complexity drops from exponential to linear.


Key Configuration Parameters

The @lru_cache decorator accepts two optional arguments:


Monitoring and Managing Cache Performance

Functions wrapped with lru_cache provide built-in methods for inspection and maintenance:


Best Practices and Limitations