Python LRU cache, provided by functools.lru_cache, reuses function results for supported hashable argument keys. It does not automatically refresh stale data, recheck authorization or copy mutable return values. This guide explains key and lifetime choices, with four local observations of cache hits, shared results and clearing.
A useful design identifies the expensive work and every input that affects its answer. This guide explains keys, object ownership, concurrency, and invalidation so a performance helper does not silently preserve stale business state.
Python LRU cache: choose results that are safe to reuse
Pure or appropriately stable computations are often easier to cache than operations with external state. A conversion based on an explicit versioned table can fit well. A function returning the current time or performing an irreversible action generally does not.
Write the reuse contract. If the same arguments should sometimes produce a different answer because permissions, configuration, or data changed, the cache needs another input or a defined invalidation process. The decorator cannot infer that requirement.
Do not cache a side-effecting function merely because it is expensive. Suppressing a second call can change application behavior. Conversely, concurrent misses can still invoke the function more than once, so the cache is not an exactly-once mechanism.
Understand the key boundary
Arguments used by lru_cache must fit its supported hashing behavior. Review positional and keyword invocation patterns, type distinctions, and the decorator’s options. Logically similar calls can have different cache identities according to documented details.
Normalize inputs only when the domain says they are equivalent. Converting several identifiers to one lowercase string can merge values that should remain distinct. A key optimization must not change business identity.
For private results, include all relevant identity or authorization context through a reviewed design. A function keyed only by record ID can reuse one user’s privileged answer for another. Caching must not bypass a current permission check.
Bound memory and result retention
Choose maxsize from measured key diversity and result size. A limited entry count is not a precise byte budget. A few large results can still retain substantial memory, and an unbounded cache can grow with workload.
The cache retains references according to documented behavior. Consider sensitive data retention and objects that indirectly hold other large structures. A cached object can outlive the request that created it.
Measure hit rate and memory under representative traffic. A cache with many unique keys may offer little reuse while consuming resources. Cache statistics help evaluate the hypothesis rather than assuming the decorator always improves performance.
Avoid mutable shared-result surprises
A cached call can return the same stored object to later callers. If one caller mutates a list or dictionary, another can observe that mutation. Decide whether the result is immutable, copied at an appropriate boundary, or not suitable for caching.
from functools import lru_cache
@lru_cache(maxsize=128)
def supported_labels(version):
return ("draft", "review", "published")
This small example returns an immutable tuple and includes a fictional version input. It illustrates shape, not a real policy registry. The function’s output and key should reflect actual version semantics in production.
Do not rely on callers remembering never to modify a shared object when correctness is consequential. Make the ownership contract visible and test it.
Define freshness and invalidation
Lru_cache is not automatically a time-to-live cache. If data must expire after a duration, use an appropriate supported mechanism or explicit policy. A size-based eviction decision does not ensure a result becomes stale-safe at a particular time.
Cache clearing can be useful but affects all relevant entries in that wrapper. Plan when it occurs and how concurrent work behaves. Avoid a global clear on every request that destroys the intended benefit.
Versioned keys can make change boundaries explicit where the data supports them. Ensure old versions do not retain sensitive or obsolete data indefinitely, and account for memory growth during transition.
Review threads and duplicate computation
Python documents thread-safe cache structure, but the wrapped function can be invoked more than once when concurrent callers arrive before an initial result is cached. Thread safety is not call-once behavior.
If repeated simultaneous work is harmful or expensive, use an appropriate synchronization or request-coalescing design. Keep it compatible with cancellation, failure, and overall deadlines. Do not put a broad lock around unrelated work without measurement.
Test the function’s behavior under duplicate misses. A cache intended for pure computation can tolerate them; a function that creates external records may not. That is another reason to avoid memoizing side effects.
Keep async and generator ownership separate
Ordinary lru_cache is not a general asynchronous-result cache. Caching coroutine or generator objects can conflict with their single-use and lifecycle semantics. Read the documentation and use a specifically supported design if asynchronous reuse is required.
Keep live resource handles out of an ordinary result cache unless ownership is carefully designed. A cached connection, response, or file can outlive its valid context or be shared unsafely between callers.
Separate reusable data from resources needed to obtain it. Cache an approved immutable representation where appropriate, while the client or connection follows its own lifecycle.
Test correctness and benefit together
Verify repeated calls, distinct keys, version changes, cache clearing, mutable-result behavior, and concurrent misses. Include unauthorized callers and data changes if the application handles private or current information.
Measure total request time and memory before and after. A high hit rate can still cache a cheap operation whose management overhead provides little benefit. Keep the optimization justified by evidence.
A practical parser caches an immutable result for a versioned public specification. A private-record endpoint instead performs current authorization and only reuses data under an approved context-aware design. The two workloads should not share an unexplained global cache policy.
Reproduce four lru_cache reuse and clearing observations
The following 4 observations were reproduced with CPython 3.13.14 on Linux using isolated local sample data, not the website server or production information. Review the script before running it in a controlled local environment.
from functools import lru_cache
import json
calls = 0
@lru_cache(maxsize=2)
def values(key):
global calls
calls += 1
return []
first = values("sample")
first.append("changed")
second = values("sample")
info = values.cache_info()
results = {
"second_result": list(second),
"underlying_calls": calls,
"cache_info": {"hits": info.hits, "misses": info.misses, "currsize": info.currsize},
}
values.cache_clear()
results["size_after_clear"] = values.cache_info().currsize
print(json.dumps(results, indent=2))
Observed output:
{
"second_result": [
"changed"
],
"underlying_calls": 1,
"cache_info": {
"hits": 1,
"misses": 1,
"currsize": 1
},
"size_after_clear": 0
}
The second call receives the cached list containing the earlier mutation; the underlying function ran once. The recorded cache information shows one hit, one miss and one retained entry, and cache_clear reduces the retained entry count to zero. The mutable return is intentional here to expose a pitfall, not a recommended production design.
This single-threaded example is not a performance benchmark, TTL implementation or exactly-once concurrency test. Clearing a cache does not invalidate references that callers already hold. Prefer immutable reusable results, and keep current authorization or external state out of an unexplained global cache policy.
Reproduce the examples: Download the six Python practice scripts (ZIP, pack 2), including this script and recorded results. No signup is required. The README records runtime and platform limits. See the Python 3.13 functools reference for documented semantics, and continue with dataclass defaults and nested mutable state.
Frequently asked questions
Does lru_cache expire data after a fixed time?
No. Its ordinary eviction policy is size and use related, not an automatic TTL contract.
Does thread safety mean one underlying call per key?
No. Concurrent misses can cause repeated computation before a result is cached.
Where should I check supported behavior?
Read the Python functools documentation. For ownership of nested mutable values, see our dataclasses guide.