Recursive functions are elegant, but they can be brutally slow. A naive recursive Fibonacci implementation makes roughly 2n function calls for an input of n. Python's standard library provides two caching decorators — functools.lru_cache and functools.cache — that store results from previous calls and return them instantly on repeats. Adding one line of code can transform an exponential algorithm into a linear one. This article covers every built-in caching approach in depth: benchmarks, cache management methods, power-of-two maxsize tuning, thread safety behavior, the keyword argument order gotcha documented in the Python source, and the conditions under which caching is actively harmful. If you are new to Python decorators, that guide covers the underlying mechanics before you apply them to caching.
Why Recursion Is Slow Without Caching
The classic recursive Fibonacci function illustrates the problem. Each call branches into two more calls, and the same subproblems get recomputed over and over:
import time
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
start = time.perf_counter()
result = fib(35)
elapsed = time.perf_counter() - start
print(f"fib(35) = {result}")
print(f"Time: {elapsed:.3f}s")
Computing fib(35) takes nearly 3 seconds because the function makes approximately 29 million calls. fib(3) alone gets called over 5 million times. Every one of those redundant calls does the same work and produces the same result. Caching eliminates the redundancy by storing each result the first time it is computed.
Manual Memoization with a Dictionary
Before reaching for a built-in decorator, it helps to understand the pattern. Memoization stores results in a dictionary keyed by the function's arguments:
import time
def fib_memo(n: int, memo: dict[int, int] | None = None) -> int:
if memo is None:
memo = {}
if n in memo:
return memo[n]
if n < 2:
return n
memo[n] = fib_memo(n - 1, memo) + fib_memo(n - 2, memo)
return memo[n]
start = time.perf_counter()
result = fib_memo(35)
elapsed = time.perf_counter() - start
print(f"fib_memo(35) = {result}")
print(f"Time: {elapsed:.6f}s")
From nearly 3 seconds to 21 microseconds. The dictionary stores each computed value, and subsequent calls with the same n return the stored result immediately. This is exactly what the built-in decorators automate.
Using None as the default and initializing the dictionary inside the function body is the correct Python pattern. A mutable default like memo={} is shared across all calls and causes stale state to persist across separate invocations — a well-known Python footgun. The built-in decorators remove the need for manual memoization entirely.
A recursive function computes fib(35) and takes nearly 3 seconds. You add a dictionary to cache results. What is the primary reason this makes it so much faster?
functools.lru_cache: Bounded Caching
functools.lru_cache wraps a function with a Least Recently Used cache. When the cache reaches its maxsize, the entry that was accessed least recently gets evicted to make room:
from functools import lru_cache
import time
@lru_cache(maxsize=128)
def fib(n: int) -> int:
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
start = time.perf_counter()
result = fib(100)
elapsed = time.perf_counter() - start
print(f"fib(100) = {result}")
print(f"Time: {elapsed:.6f}s")
Without caching, fib(100) would require approximately 1020 function calls and would never finish. With @lru_cache, it completes in under a millisecond because each value from fib(0) through fib(100) is computed exactly once.
The maxsize parameter defaults to 128. Setting it to None disables the size limit, making the cache grow without bound. Since Python 3.8, you can also apply @lru_cache without parentheses to use the default maxsize:
The Python documentation states that the LRU feature performs best when maxsize is a power of two (docs.python.org/3/library/functools.html). The default of 128 already satisfies this: it is 27. If you need a custom size, choose 32, 64, 256, or 512 rather than an arbitrary integer. As Luciano Ramalho documents in Fluent Python, 2nd Edition (O'Reilly, 2022), this recommendation comes directly from the CPython implementation.
Here is why: CPython's lru_cache is implemented in C and uses a circular doubly linked list combined with a hash table. The linked list maintains access order — the most recently used entry sits adjacent to the root, the least recently used sits just before it. When the cache is full, CPython reuses the root node itself as the new entry rather than allocating a new one, which makes eviction O(1) with zero allocation cost. The hash table that maps argument tuples to list nodes uses bitmask-based indexing, which only aligns cleanly when the table size is a power of two. A non-power-of-two maxsize does not break the cache — it works correctly — but it foregoes this internal alignment optimization (confirmed in CPython source: Modules/_functoolsmodule.c).
One more edge case worth knowing: passing a negative integer as maxsize does not raise an error. CPython silently normalizes it to zero, which disables the cache entirely — every call is treated as a miss and the underlying function always executes. This is a documented behavior in the CPython source but easy to miss in practice.
The Python documentation explicitly warns that calls differing only in keyword argument order — such as f(a=1, b=2) versus f(b=2, a=1) — may produce two separate cache entries (docs.python.org/3/library/functools.html). This is a genuine gotcha. If two callers pass the same arguments but in a different keyword order, both will miss the cache and execute the function independently. To avoid this, always pass arguments positionally to a cached function, or always use keyword arguments in a consistent order.
from functools import lru_cache
@lru_cache
def factorial(n: int) -> int:
return n * factorial(n - 1) if n else 1
print(factorial(10))
functools.cache: Unbounded Caching
Python 3.9 introduced functools.cache as a simpler alternative to lru_cache(maxsize=None). It is a thin wrapper around a dictionary lookup with no eviction logic, making it slightly faster for unbounded use cases:
from functools import cache
@cache
def fib(n: int) -> int:
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
print(fib(50))
print(fib(100))
lru_cache was added in Python 3.2 (typed option in 3.3; user_function bare-decorator form in 3.8). cache was added in Python 3.9 as a simpler unbounded-only variant. If you need to support Python 3.2–3.8, use lru_cache.
lru_cache accepts a maxsize integer (default 128). When full, the least recently used entry is evicted. Set to None to disable eviction. cache has no size limit — it grows without bound for the lifetime of the process or until explicitly cleared.
lru_cache evicts the least recently used entry when the cache exceeds maxsize. This keeps hot entries in memory and lets cold ones age out. cache has no eviction — every unique call result stays cached indefinitely.
lru_cache maintains bookkeeping structures to track access order for eviction — a small but real cost on every call. cache is a thin wrapper around a plain dictionary lookup with no eviction bookkeeping, making it slightly faster per call.
cache is functionally identical to lru_cache(maxsize=None) but implemented with less overhead because it skips all the LRU eviction machinery. The two are interchangeable in behavior; cache is preferred when maxsize=None is what you want.
lru_cache is the right choice for long-running processes, web servers, or any context where memory must be bounded and old entries should age out automatically. cache suits recursive algorithms and short scripts where the full input space is small enough to hold in memory indefinitely.
You are writing a recursive algorithm for a short script. The input values are bounded and you want the lowest possible overhead. Which decorator is the better choice?
cache_info() and cache_clear()
Both decorators add two management methods to the decorated function. cache_info() returns a named tuple with performance statistics, and cache_clear() empties the cache:
from functools import lru_cache
@lru_cache(maxsize=64)
def fib(n: int) -> int:
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
fib(50)
print(fib.cache_info())
fib.cache_clear()
print(fib.cache_info())
The hits field shows how many calls returned a cached result. The misses field shows how many calls computed a new result. For fib(50), there are 51 unique inputs (0 through 50), each computed once (51 misses), and 48 calls that hit the cache (the recursive second branch for each n >= 2).
Python 3.9 also added cache_parameters(), which returns the decorator's configuration as a plain dictionary:
from functools import lru_cache
@lru_cache(maxsize=64)
def fib(n: int) -> int:
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
fib(20)
print(fib.cache_info()) # hits, misses, maxsize, currsize
print(fib.cache_parameters()) # {'maxsize': 64, 'typed': False}
The Python documentation confirms that both lru_cache and cache are thread-safe: the underlying data structure remains coherent during concurrent updates (docs.python.org/3/library/functools.html). However, the same documentation notes an important nuance: if a second thread calls the function before the first thread's call completes and is cached, the wrapped function may execute more than once for that input. Thread safety covers data structure integrity — not call deduplication under concurrent load. The cache also keeps live references to both argument values and return values for the entire time an entry is cached, which matters if your function returns large objects such as NumPy arrays or DataFrames.
You can also access the original unwrapped function through __wrapped__, which is useful for testing or benchmarking without the cache:
# Call the original uncached function directly
print(fib.__wrapped__(10))
You call fib(50) on an @lru_cache(maxsize=64)-decorated function. Afterward, fib.cache_info() shows hits=48, misses=51. What do these numbers tell you?
The typed Parameter
By default, lru_cache treats arguments of different types as equivalent if they compare equal. This means fib(3) and fib(3.0) share the same cache entry. Setting typed=True separates them:
from functools import lru_cache
@lru_cache(maxsize=128, typed=True)
def compute(x: int | float) -> int | float:
print(f" Computing for {x!r} (type: {type(x).__name__})")
return x * 2
print(compute(3))
print(compute(3.0))
print(compute(3))
With typed=True, the integer 3 and the float 3.0 are cached separately. The third call returns the cached integer result without recomputing.
The official documentation includes a subtle qualifier that is worth knowing: when typed=False, the implementation will usually treat equal-but-differently-typed arguments as the same cache key — but not always. The docs note that certain types, including str and int, may be cached separately even when typed is false (docs.python.org/3/library/functools.html). This is an implementation detail of CPython, not a guarantee of the language specification. If exact type-based cache separation matters for correctness in your code, set typed=True explicitly rather than relying on this behavior.
You decorate a function with @lru_cache(maxsize=128) (no typed argument). What happens when you call it with compute(4) and then compute(4.0)?
Recursion Depth and sys.setrecursionlimit
Python's default recursion limit is 1000. Even with caching, a deeply recursive first call can exceed this limit because the cache is empty and every level recurses before any result is stored. You can increase the limit with sys.setrecursionlimit, but the safer approach is to warm the cache incrementally:
from functools import cache
@cache
def fib(n: int) -> int:
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
# Warm the cache in safe increments
for i in range(0, 5001, 250):
fib(i)
# Now fib(5000) hits cached values, no deep recursion
print(f"fib(5000) has {len(str(fib(5000)))} digits")
Calling sys.setrecursionlimit() to a very high value can crash the Python process by overflowing the C stack. Incremental cache warming is the safer approach for deeply recursive functions. Build up cached values in small batches so that subsequent calls never need to recurse more than a few hundred levels.
You have an @cache-decorated fib function and call fib(2000) directly, but Python raises a RecursionError. What is the safest fix?
Caching Beyond Fibonacci: Dynamic Programming
The caching decorators apply to any recursive algorithm with overlapping subproblems. Here are two additional examples. For a related pattern that pairs recursion with lazy evaluation, see the guide on recursive generators in Python.
Stair Climbing Problem
from functools import cache
@cache
def climb_stairs(n: int) -> int:
"""Count ways to climb n stairs taking 1, 2, or 3 steps at a time."""
if n < 0:
return 0
if n == 0:
return 1
return climb_stairs(n - 1) + climb_stairs(n - 2) + climb_stairs(n - 3)
print(f"Ways to climb 10 stairs: {climb_stairs(10)}")
print(f"Ways to climb 30 stairs: {climb_stairs(30)}")
Minimum Edit Distance (Levenshtein)
from functools import cache
@cache
def edit_distance(s: str, t: str) -> int:
"""Compute the minimum edit distance between two strings."""
if not s:
return len(t)
if not t:
return len(s)
if s[-1] == t[-1]:
return edit_distance(s[:-1], t[:-1])
return 1 + min(
edit_distance(s[:-1], t), # deletion
edit_distance(s, t[:-1]), # insertion
edit_distance(s[:-1], t[:-1]), # substitution
)
print(edit_distance("kitten", "sitting"))
print(edit_distance("saturday", "sunday"))
print(edit_distance.cache_info())
For string-based recursive algorithms like edit distance, the cache key is the tuple of all arguments. Strings are hashable in Python, so they work directly with lru_cache. If your function takes a list, convert it to a tuple before caching.
You want to use @lru_cache on a function that takes a string argument. Which of the following is true?
Beyond the Decorator: Deeper Caching Strategies
The decorator approach solves the general case, but certain contexts demand more deliberate design. Four patterns come up repeatedly when the built-in decorators are not sufficient on their own.
Bottom-Up Iteration Instead of Top-Down Recursion
The decorator approach is top-down: start from the target value and recurse downward, caching results on the way back up. An alternative is to build the solution iteratively from the smallest subproblem upward. This eliminates the call stack entirely, removes any risk of hitting the recursion limit regardless of input size, and uses O(1) memory for problems where only the last few values are needed at each step:
def fib_iterative(n: int) -> int:
"""Bottom-up Fibonacci: O(n) time, O(1) memory, no call stack."""
if n < 2:
return n
a, b = 0, 1
for _ in range(2, n + 1):
a, b = b, a + b
return b
# No recursion limit, no decorator overhead, no cache memory usage
print(fib_iterative(10_000)) # works without any sys.setrecursionlimit
For Fibonacci specifically, the iterative version uses two variables instead of a growing cache. For more complex DP problems — knapsack, longest common subsequence, coin change — the equivalent is filling a table row by row. The decorator is a shortcut that keeps the recursive structure readable; the iterative approach is what you reach for when the input range is unbounded or memory is constrained.
Class-Level Shared Cache
When a cached function is defined as a method on a class, @lru_cache and @cache behave differently than expected. Both decorators include self in the cache key, which means each instance maintains its own cache — and the instance cannot be garbage collected as long as the cache holds a reference to it. For algorithms shared across many instances, this means duplicated computation and memory growth proportional to instance count.
The idiomatic fix is to move the cached function to the module level or a static method, accepting arguments explicitly rather than through self:
from functools import cache
# Shared module-level cache — one cache for all callers
@cache
def _edit_distance(s: str, t: str) -> int:
if not s:
return len(t)
if not t:
return len(s)
if s[-1] == t[-1]:
return _edit_distance(s[:-1], t[:-1])
return 1 + min(
_edit_distance(s[:-1], t),
_edit_distance(s, t[:-1]),
_edit_distance(s[:-1], t[:-1]),
)
class Spellchecker:
def distance(self, word: str, candidate: str) -> int:
# Delegates to shared cache — no per-instance duplication
return _edit_distance(word, candidate)
a = Spellchecker()
b = Spellchecker()
# Both calls hit the same cache — computed once regardless of instance
print(a.distance("kitten", "sitting")) # 3
print(b.distance("kitten", "sitting")) # 3 — cache hit
print(_edit_distance.cache_info()) # hits=1, misses=1 (not hits=0, misses=2)
Applying @lru_cache directly to an instance method causes the cache to hold a strong reference to self, preventing garbage collection for the lifetime of the cache entry. This is not a hypothetical concern — it is the standard reason cached instance methods cause memory leaks in long-running Python services, and is flagged as rule B019 in the Ruff linter (docs.astral.sh/ruff/rules/cached-instance-method/). Bloomberg's Memray memory profiler documentation includes it as a tutorial exercise precisely because it is a common production bug.
There are two corrective patterns. The first (shown above) is to move the cached logic to module level and delegate to it. The second, documented in the Python FAQ under "How do I cache method calls?", is to bind the cache per instance in __init__:
import functools
class Spellchecker:
def __init__(self):
# Bind lru_cache to the instance in __init__ — no self in cache key,
# no global strong reference, cache lifetime tied to instance lifetime
self.distance = functools.lru_cache(maxsize=256)(self._distance_impl)
def _distance_impl(self, word: str, candidate: str) -> int:
"""Core edit-distance logic — called through self.distance()."""
if not word:
return len(candidate)
if not candidate:
return len(word)
if word[-1] == candidate[-1]:
return self._distance_impl(word[:-1], candidate[:-1])
return 1 + min(
self._distance_impl(word[:-1], candidate),
self._distance_impl(word, candidate[:-1]),
self._distance_impl(word[:-1], candidate[:-1]),
)
checker = Spellchecker()
print(checker.distance("kitten", "sitting")) # 3
print(checker.distance.cache_info()) # per-instance cache
# When checker goes out of scope, cache and instance are collected together
The __init__-binding pattern gives each instance its own cache, so different instances with different internal state do not share cached results. The module-level pattern gives a global shared cache — correct when the computation is stateless, wrong when it depends on instance attributes.
Weak-Reference Caching for Large Return Values
Standard lru_cache holds strong references to cached return values, keeping them alive regardless of whether anything else in the program is using them. For functions that return large objects — NumPy arrays, DataFrames, parsed ASTs — this can hold gigabytes of memory that the garbage collector cannot reclaim even when the data is no longer needed anywhere else.
A manual cache backed by weakref.WeakValueDictionary stores weak references instead of strong ones. The cached value is kept alive as long as at least one strong reference exists elsewhere in the program. When the last strong reference drops, the garbage collector reclaims the object and the cache entry is removed automatically:
import weakref
_parse_cache: weakref.WeakValueDictionary = weakref.WeakValueDictionary()
def parse_config(path: str) -> dict:
"""Cache parsed config objects weakly — GC can reclaim them."""
if path in _parse_cache:
return _parse_cache[path]
# Simulate an expensive parse
result = {"path": path, "data": list(range(1_000_000))}
_parse_cache[path] = result
return result
config = parse_config("/etc/app.conf")
print(len(_parse_cache)) # 1 — entry exists while config is in scope
del config
import gc; gc.collect()
print(len(_parse_cache)) # 0 — entry removed when strong reference dropped
The trade-off is that a cache hit is not guaranteed even for a key that was previously inserted — if no other strong reference exists at the time of the lookup, the entry will have been collected. Use weak-reference caching when the cost of recomputing is acceptable and memory pressure is the primary concern, not when every cache hit must be guaranteed.
Avoiding the Recomputation Window with an External Lock
The thread-safety nuance documented for lru_cache — that the wrapped function may execute more than once under concurrent load before the first result is cached — matters in a specific class of problems: expensive computations where double-execution has a real cost, such as a recursive function that reads from disk or makes a network call on cache miss. The decorator alone cannot prevent this. Wrapping the call in a threading.Lock serializes concurrent requests for the same key:
import threading
from functools import lru_cache
_lock = threading.Lock()
@lru_cache(maxsize=256)
def _expensive_recursive(n: int) -> int:
if n < 2:
return n
return _expensive_recursive(n - 1) + _expensive_recursive(n - 2)
def safe_recursive(n: int) -> int:
"""Serialize concurrent first-calls to prevent duplicate execution."""
with _lock:
return _expensive_recursive(n)
# safe_recursive ensures only one thread computes each unique n first
print(safe_recursive(50)) # 12586269025
This is intentional over-engineering for pure in-memory recursion — the lock contention cost would outweigh any benefit when the computation itself is microseconds. It is the right pattern when a cache miss triggers I/O or a database call, where executing twice has a real cost beyond CPU cycles.
The built-in decorators cover the common cases well, but three needs they cannot meet are: cache expiration (TTL), alternative eviction policies (LFU, FIFO, random replacement), and per-method instance caches without manual wiring. The third-party cachetools library (cachetools.readthedocs.io) fills these gaps with a compatible API. It provides LRUCache, LFUCache, TTLCache, FIFOCache, and a @cachedmethod decorator that takes a cache accessor function — meaning the cache lives on the instance, not globally. For long-running services where stale cache entries are a correctness risk, cachetools.TTLCache is the standard upgrade path from functools.lru_cache.
When Not to Cache
Caching is not always appropriate. The Python documentation states that functions with side effects, functions that must produce distinct mutable objects on each call — such as generators and async functions — and impure functions like time() or random() are not suitable candidates (docs.python.org/3/library/functools.html). More broadly, avoid caching any function whose output depends on external state — a database query that might return different results over time, for instance, will silently return stale data once its result is cached. Also avoid caching functions that accept a huge number of unique inputs with no repetition, where the cache grows without ever producing a hit.
from functools import lru_cache
@lru_cache
def bad_candidate(items):
"""This will fail because lists are not hashable."""
return sum(items)
try:
bad_candidate([1, 2, 3])
except TypeError as e:
print(f"Error: {e}")
# Fix: accept a tuple — convert at the call site before passing in
@lru_cache
def good_candidate(items: tuple[int, ...]) -> int:
"""items must be a tuple — lists are not hashable."""
return sum(items)
my_list = [1, 2, 3]
print(good_candidate(tuple(my_list)))
You want to cache a function that receives a list of integers. What must you do before passing the argument to an @lru_cache-decorated function?
Key Takeaways
- Memoization eliminates redundant computation: Recursive functions with overlapping subproblems compute the same values repeatedly. Caching stores each result once and returns it on subsequent calls, reducing exponential time complexity to linear.
- functools.lru_cache is the standard tool: Available since Python 3.2, it provides a configurable bounded cache with LRU eviction. The default
maxsizeis 128. Set it toNonefor unbounded caching. - functools.cache is the simpler option: Available since Python 3.9, it is equivalent to
lru_cache(maxsize=None)but faster because it skips eviction bookkeeping. Use it for recursive algorithms where you know the input space fits in memory. - Use cache_info() to monitor performance: The
hits,misses,maxsize, andcurrsizefields tell you how effectively the cache is working. High miss rates suggest the cache is too small or the function is not being called with repeated arguments. - Warm the cache to avoid hitting the recursion limit: For deeply recursive functions, call the function with incrementally larger inputs so that the cache builds up layer by layer. This avoids
RecursionErrorwithout increasingsys.setrecursionlimit. - Consider bottom-up iteration when input range is unbounded: Iterative DP eliminates the call stack entirely, removes recursion limit risk at any input size, and can reduce memory to O(1) for rolling-window problems where only a few prior values are needed at each step.
- Never apply @lru_cache directly to an instance method: The decorator includes
selfin the cache key, prevents garbage collection of instances, and duplicates computation across objects. Move the cached function to module level and delegate to it from the method. - Use WeakValueDictionary for functions returning large objects: Standard
lru_cacheholds strong references, keeping large return values in memory indefinitely. A manual cache backed byweakref.WeakValueDictionaryallows the garbage collector to reclaim entries when no other strong reference exists. - Only cache deterministic, pure functions: Functions with side effects, non-deterministic output, or unhashable arguments are not suitable candidates for caching. The cached result must be correct for every future call with the same inputs.
Python's built-in caching decorators are the simplest and most reliable way to speed up recursive functions. One decorator, one import, and the algorithm's time complexity drops by orders of magnitude.
How to Add Caching to a Recursive Python Function
Step 1: Import the caching decorator
Add from functools import lru_cache or from functools import cache at the top of your file. lru_cache is available from Python 3.2; cache requires Python 3.9 or later.
Step 2: Apply the decorator to your recursive function
Place @lru_cache(maxsize=128) or @cache directly above your function definition. No changes to the function body are required. The decorator intercepts every call and checks the cache before executing the function.
Step 3: Ensure all arguments are hashable
Both decorators use a dictionary as the backing store, so all arguments must be hashable. Convert any lists to tuples and any sets to frozensets before passing them to a cached function to avoid a TypeError.
Step 4: Check cache performance with cache_info()
Call your_function.cache_info() after running the function. The returned named tuple shows hits, misses, maxsize, and currsize. A high hit rate confirms the cache is working effectively. If misses dominate, consider whether the function is being called with repeated arguments at all.
Step 5: Clear the cache when needed
Call your_function.cache_clear() to remove all stored entries and reset the statistics. This is useful between test runs or when input data changes and previously cached results are no longer valid.
Step 6: Warm the cache for deeply recursive inputs
For inputs that would exceed Python's default recursion limit of 1000, call the function with incrementally larger values in a loop before targeting the final value. This builds cached entries layer by layer without triggering a RecursionError, which is safer than raising sys.setrecursionlimit to an extreme value.
Frequently Asked Questions
What is the difference between functools.cache and functools.lru_cache?
functools.cache (Python 3.9+) is an unbounded cache with no size limit. It is equivalent to lru_cache(maxsize=None) but slightly faster because it skips the LRU eviction bookkeeping. functools.lru_cache has a configurable maxsize (default 128) and evicts least recently used entries when the cache is full. Use cache for recursive functions with a bounded input space, and lru_cache when you need to control memory usage.
How does lru_cache speed up recursive functions?
Recursive functions like Fibonacci recompute the same subproblems repeatedly, resulting in exponential time complexity. lru_cache stores the result of each unique input the first time it is computed. On subsequent calls with the same input, the cached result is returned immediately without re-executing the function, reducing the time complexity to linear.
Does lru_cache work with unhashable arguments?
No. Both lru_cache and cache use a dictionary internally, so all function arguments must be hashable. Passing mutable types like lists, dictionaries, or sets as arguments raises a TypeError. Convert them to tuples or frozensets before passing them to a cached function.
How do I clear the cache of a decorated function?
Call the cache_clear() method on the decorated function. For example, fibonacci.cache_clear() removes all cached entries. You can also inspect cache performance with cache_info(), which returns a named tuple showing hits, misses, maxsize, and currsize. Python 3.9 added cache_parameters(), which returns the maxsize and typed settings as a plain dictionary — useful for logging or introspection without mutating the cache.
Is lru_cache thread-safe?
Yes, with an important nuance. The Python documentation confirms both lru_cache and cache are thread-safe in that the underlying data structure remains coherent during concurrent updates. However, the docs also note that if a second thread calls the function before the first thread's call finishes and is stored in the cache, the wrapped function may execute more than once for that input. Thread safety covers structural integrity, not call deduplication. For workloads where double-execution is unacceptable, you need an external lock.
Does keyword argument order affect lru_cache?
Yes — this is a documented gotcha. The Python documentation notes that calls differing only in keyword argument order may be treated as distinct cache keys and produce separate entries. In practice this means two callers passing the same values but in a different keyword order will both miss the cache. To avoid this, pass arguments positionally to cached functions, or always use keyword arguments in a consistent order.
Sources
Python Software Foundation — functools documentation
All technical specifications, version history, thread safety behavior, keyword argument ordering notes, and the power-of-two maxsize recommendation are drawn from the official Python documentation: docs.python.org/3/library/functools.html. The documentation is maintained by the Python Software Foundation and updated with each CPython release.
Fluent Python, 2nd Edition — Luciano Ramalho (O'Reilly, 2022)
The power-of-two maxsize recommendation and practical guidance on lru_cache performance characteristics are also documented in Luciano Ramalho's Fluent Python, 2nd Edition (O'Reilly Media, 2022), which cross-references the CPython implementation directly.
CPython source — Modules/_functoolsmodule.c
Internal data structure details — including the circular doubly linked list used by lru_cache, root-node reuse for O(1) eviction, and negative maxsize normalization — are verifiable in the CPython source repository at github.com/python/cpython.
Ruff linter — rule B019 (cached-instance-method)
The instance method memory leak pattern is formally documented as a lint rule in the Ruff linter: docs.astral.sh/ruff/rules/cached-instance-method/. The rule flags any use of @lru_cache or @cache directly on an instance method.
cachetools — Extensible memoizing collections and decorators
The cachetools library extends the standard library's caching decorators with TTL-aware caches, alternative eviction policies, and per-instance method caching: cachetools.readthedocs.io.