Back to Blog
Python

Using Python cachetools: TTLCache, LRUCache, and the cached Decorator

python cachetools ttlcache lrucache and cached decorators: Learn how to use cachetools for efficient caching in Python: TTLCache for time-based expiration, LRUCache fo...

cachetoolscachingPythondecoratorsperformance
Illustration of a Python cache with a clock and a gauge, representing TTLCache time expiration and LRUCache size limits.

python cachetools ttlcache lrucache and cached decorators requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When a Python function performs expensive work—database queries, API calls, or heavy computation—repeating that work for identical inputs wastes time and resources. The cachetools library provides ready-made cache classes and a decorator that lets you add caching with minimal code. This article covers the two most common cache types, TTLCache and LRUCache, and the cached decorator that ties them to your functions.

Installing cachetools and Basic Usage

cachetools is a third-party library, so install it with pip:

pip install cachetools

The library provides several cache classes, all implementing a common interface. The two you will use most often are TTLCache and LRUCache. You can use them directly as dictionary-like objects:

from cachetools import TTLCache, LRUCache # A cache that holds up to 100 items, each valid for 300 seconds ttl_cache = TTLCache(maxsize=100, ttl=300) # A cache that holds up to 100 items, evicting least recently used ones lru_cache = LRUCache(maxsize=100) # Both support standard dict operations ttl_cache["key"] = "value" print(ttl_cache["key"]) # "value"

These classes are not just simple dictionaries—they enforce size limits and expiration policies automatically. When you insert beyond maxsize, the cache evicts items according to its policy. For TTLCache, items expire after ttl seconds; for LRUCache, the least recently accessed item is removed.

The cached Decorator for Function Results

Instead of managing cache objects manually, cachetools provides a cached decorator that wraps a function and stores its return values. The decorator accepts a cache instance as its first argument:

from cachetools import cached, TTLCache cache = TTLCache(maxsize=128, ttl=600) @cached(cache) def get_user(user_id): # Simulate a slow database lookup print(f"Fetching user {user_id} from database") return {"id": user_id, "name": "Alice"} print(get_user(1)) # Fetches and caches print(get_user(1)) # Returns from cache, no print output

The decorator uses the function arguments to build a cache key. By default, it uses a hash of the positional and keyword arguments. This works well for immutable arguments like strings, numbers, and tuples. If your function accepts mutable objects like lists or dicts, you need to provide a custom key function, as shown later.

TTLCache: Time-Based Expiration

TTLCache is ideal when cached data becomes stale after a fixed period. Each entry stores an expiration timestamp based on the ttl value you provide. When you access an expired item, cachetools treats it as missing and raises KeyError or returns None depending on the method used. The cached decorator then recomputes the function result and stores it again.

from cachetools import cached, TTLCache import time cache = TTLCache(maxsize=10, ttl=2) @cached(cache) def get_temperature(city): return 22.5 # Simulated API call print(get_temperature("Berlin")) # 22.5 time.sleep(3) print(get_temperature("Berlin")) # Recomputes because entry expired

TTLCache also supports per-item TTL override via the ttl parameter in the __setitem__ method, but the cached decorator uses the cache's default ttl. If you need different expiration times for different calls, you would need a custom cache or a separate decorator.

LRUCache: Size-Based Eviction

LRUCache keeps the most recently used items and discards the least recently used when the cache reaches maxsize. This is useful when you want to bound memory usage and the recency of access correlates with future usefulness. Unlike TTLCache, items never expire by time; they stay until evicted by size.

from cachetools import cached, LRUCache cache = LRUCache(maxsize=3) @cached(cache) def compute(n): print(f"Computing {n}") return n * n compute(1) # Computes compute(2) # Computes compute(3) # Computes compute(1) # Returns from cache (1 is most recent now) compute(4) # Computes; evicts 2 (least recently used)

After inserting 1, 2, 3, the cache holds all three. Accessing 1 makes it the most recent. Adding 4 exceeds maxsize, so the least recently used item (2) is evicted.

Comparing TTLCache and LRUCache

The choice between these two caches depends on the nature of your data and access patterns. The table below summarizes the key differences:

CriterionTTLCacheLRUCache
Eviction triggerTime since insertionSize of cache
ExpirationYes, after ttl secondsNo, items remain until evicted
Memory boundYes, via maxsizeYes, via maxsize
Best forData that becomes stale quicklyData that stays valid, frequent reuse
Access patternAny, but time-based expiry neededRecency-based reuse

Use TTLCache when the underlying data changes over time—for example, a weather forecast or a session token. Use LRUCache when the data is immutable and you want to avoid recomputation for frequently repeated inputs, like a memoized Fibonacci function or configuration lookups.

Practical Considerations: Thread Safety and Memory

cachetools caches are not thread-safe by default. If your application uses multiple threads and accesses the same cache concurrently, you need to protect it with a lock. The cached decorator does not add locking; you must manage synchronization yourself. A common pattern is to use a threading.RLock around cache operations:

import threading from cachetools import cached, TTLCache cache = TTLCache(maxsize=100, ttl=60) lock = threading.RLock() @cached(cache) def get_data(key): with lock: # Simulate expensive operation return key * 2

This approach ensures that concurrent calls to get_data do not corrupt the cache. However, it serializes all cache accesses, which may become a bottleneck. For high-concurrency scenarios, consider a cache that is designed for thread safety, such as cachetools's RRCache or an external cache like Redis.

Memory usage is another concern. TTLCache and LRUCache both hold references to keys and values. If your cached objects are large, the cache can consume significant memory. The maxsize parameter limits the number of entries, not the total memory footprint. For memory-bound applications, you can use cachetools's getsizeof parameter to estimate object sizes and evict based on total size, but that is only available in certain cache classes like LFUCache and RRCache. For TTLCache and LRUCache, you must estimate memory usage manually and choose maxsize accordingly.

Custom Cache Keys and Advanced Usage

The cached decorator builds a key from the function arguments using a default hash. For functions that accept mutable arguments like lists or dictionaries, the default key generation will raise a TypeError because unhashable types cannot be used. You can supply a custom key function to the decorator:

from cachetools import cached, TTLCache cache = TTLCache(maxsize=100, ttl=300) @cached(cache, key=lambda *args, **kwargs: args[0]) def process_items(items): # items is a list, which is unhashable return sum(items) print(process_items([1, 2, 3])) # Works

The key function receives the same arguments as the decorated function and must return a hashable value. In this example, we use only the first positional argument, but you could combine arguments into a tuple or a string.

Another advanced use case is to bypass the cache for specific calls. The cached decorator does not provide a direct way to skip caching, but you can achieve this by calling the underlying function directly if you store a reference to it before decoration:

from cachetools import cached, TTLCache cache = TTLCache(maxsize=100, ttl=60) def _expensive(x): return x * x @cached(cache) def expensive(x): return _expensive(x) # Force recomputation by calling the inner function print(_expensive(5)) # Always recomputes

This pattern is useful when you need fresh data occasionally but still want caching for the common path.

Choosing the Right Cache Type for Your Function

The decision between TTLCache and LRUCache ultimately depends on your data's validity and access pattern. If you know that a value is only valid for a short period, TTLCache prevents serving stale data. If your data is immutable and you want to maximize hit rate under a fixed memory budget, LRUCache is the better fit. For many real-world scenarios, a combination of both—using TTLCache with a generous maxsize and a moderate ttl—provides a good balance: it bounds memory while ensuring that entries eventually expire.

When you need both time-based expiration and size-based eviction, cachetools also offers TLRUCache (time-aware LRU), which combines both policies. This can be a better choice than layering two caches manually. The cached decorator works with any cache class that implements the required interface, so you can swap cache types without changing your function code.

Finally, remember that caching adds complexity to debugging. When a function's result is cached, changes to external state may not be reflected. Always consider whether caching is appropriate for the function's semantics. For functions with side effects or that depend on global mutable state, caching can lead to subtle bugs. Use caching only for pure functions that return deterministic results based on their arguments.

python cachetools ttlcache lrucache and cached decorators: P | RYUSLOG DEV