cachetools review
cachetools 7.1.7 supplies bounded in-process mappings with LRU, LFU, FIFO, random-replacement, fixed-TTL, and per-item time-to-use eviction. Its `cached` and `cachedmethod` decorators memoize ordinary Python callables while exposing the underlying cache, key function, lock, and hit statistics. Version 7.1 added packaged type stubs; 7.1.7 fixes replacement of an existing entry when the new value has a larger reported size. Our fresh install was only 1 MB with no dependencies, so the decision is about cache semantics rather than package weight.
cachetools 7.1.7 installed in 0.2 seconds as one 1 MB package with zero dependencies and no audit findings, making it an inexpensive choice for bounded cache state inside one Python process. Do not install it as a substitute for a shared cache or for async memoization without an async-aware wrapper.
We installed it
| Install | ✓ · 0.2s | 1 package on disk · 1 MB |
| Import | ✓ | import cachetools in 0.05s · pure Python · py.typed · requires Python >=3.10 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does cachetools install cleanly?
Yes. In a fresh container with an empty cache, pip install cachetools finished in 0.2s, leaving 1 package and 1 MB on disk. pip-audit reported no known vulnerabilities.
What does cachetools need to run?
Python >=3.10, and nothing compiled: it is pure Python. In our run import cachetools succeeded in 0.05s, and the package ships py.typed for type checkers.
cachetools or cacheout: which should you use?
cacheout: Use it when a small in-memory cache with defaults, callbacks, and bulk operations matters more than choosing among several eviction algorithms. cachetools 7.1.7 installed in 0.2 seconds as one 1 MB package with zero dependencies and no audit findings, making it an inexpensive choice for bounded cache state inside one Python process.
When should you not use cachetools?
Several workers or hosts must share cached values. cachetools stores Python objects only inside the current process; Redis, Memcached, or a database-backed cache fits that job.
Use it if
- A single Python process needs a hard item or value-size limit instead of the unbounded behavior of a plain dictionary.
- You need TTL or TLRU expiration, LFU or random eviction, or an inspectable cache behind a function decorator.
- Tests need a custom clock so expiry can be advanced without sleeping.
- A method cache must be selected from each instance and cleared or resized while the application is running.
- Several workers or hosts must share cached values. cachetools stores Python objects only inside the current process; Redis, Memcached, or a database-backed cache fits that job.
- Untrusted callers can produce unlimited distinct keys and you cannot choose a meaningful `maxsize` or `getsizeof`. Eviction limits storage, but repeated misses can still drive backend work.
- Code mutates cached values after insertion and expects `currsize` to follow. The documentation says `getsizeof` runs only when an item is inserted, so later growth is not counted.
- You plan to share one cache across threads without synchronization. The cache classes are explicitly not thread-safe; decorators need a lock or condition supplied by the caller.
- Expired objects must release memory at the exact TTL boundary. `TTLCache` removes expired entries on the next mutating operation or an explicit `expire()` call, not from a background timer.
Setup reality
We installed cachetools 7.1.7 in a fresh Python 3.12 Bookworm container. The install finished in 0.2 seconds, left one package and 1 MB on disk, and added zero direct dependencies. import cachetools completed in 0.05 seconds. The distribution is pure Python, requires Python 3.10 or newer, includes py.typed, and pip-audit found no known vulnerabilities in our sandbox. PyPI did not publish a license value, although the repository carries an MIT license file.
There are no credentials, service processes, or config files. You construct a cache in code and must set maxsize; the default size function counts every value as 1. Pass getsizeof when bytes or another cost should control eviction. That callback runs at insertion, so treat cached values as immutable or replace them after mutation. Version 7.1.7 specifically corrected replacement when an existing key receives a larger value.
TTLCache uses time.monotonic by default and calculates expiry from timer() + ttl. Reads treat expired keys as absent, but expired objects can remain allocated until a write occurs. Call expire() during an idle maintenance point if prompt reclamation matters. A custom timer makes deterministic tests possible, and TLRUCache accepts a ttu(key, value, now) function for per-item deadlines.
The cache mappings are not thread-safe. cached and cachedmethod accept a lock, while a condition can also prevent several threads from recomputing the same missing key. Locking protects cache access, not the wrapped function itself. Async functions need an async-aware wrapper such as asyncache; decorating a coroutine with the synchronous helper can cache coroutine objects instead of completed results.
Patterns
Keep the 256 most recent results cache-lru-values
from cachetools import LRUCache
results = LRUCache(maxsize=256)
results['job-42'] = {'state': 'done'}
state = results.get('job-42')`maxsize=256` counts entries because the default `getsizeof` returns 1 for every value.
Memoize a pure function memoize-function
from cachetools import LRUCache, cached
cache = LRUCache(maxsize=1_024)
@cached(cache)
def parse_schema(text: str):
return expensive_parse(text)Arguments must be hashable under the selected key function. The exposed `cache` mapping can be cleared directly.
Cache API results for 5 minutes expire-after-ttl
from cachetools import TTLCache, cached
responses = TTLCache(maxsize=500, ttl=300)
@cached(responses)
def fetch_user(user_id: int):
return api.get_user(user_id)The 300-second deadline uses a monotonic clock. Expired values may stay allocated until a mutation or `responses.expire()`.
Remove expired entries explicitly release-expired-memory
expired = list(responses.expire())
for key, value in expired:
close_if_needed(value)`expire()` returns the removed key-value pairs in 7.1.7, which is useful when values own resources or memory must be reclaimed during an idle period.
Evict by reported byte length limit-by-value-size
from cachetools import LRUCache
blobs = LRUCache(8 * 1024 * 1024, getsizeof=len)
blobs['avatar'] = image_bytesThe 8 MB limit uses `len(value)` only when assigning an entry. Mutating a stored byte-like object later does not update `currsize`.
Protect a memoized cache across threads make-thread-safe
from threading import RLock
from cachetools import LRUCache, cached
lock = RLock()
shared = LRUCache(maxsize=512)
@cached(shared, lock=lock)
def load_account(account_id: int):
return database.load(account_id)The lock wraps cache operations only. The function body runs outside the lock, so duplicate work on a simultaneous miss is still possible.
Coalesce concurrent misses prevent-cache-stampede
from threading import Condition
from cachetools import TTLCache, cached
condition = Condition()
cache = TTLCache(maxsize=200, ttl=60)
@cached(cache, condition=condition)
def render_report(report_id: int):
return build_report(report_id)A condition makes threads with the same missing key wait for the first calculation, reducing a cache stampede.
Give each instance its own method cache cache-method
from cachetools import LRUCache, cachedmethod
from operator import attrgetter
class Catalog:
def __init__(self):
self.cache = LRUCache(maxsize=128)
@cachedmethod(attrgetter('cache'))
def item(self, item_id: int):
return self.read_item(item_id)cachetools 7 implements `cachedmethod` as a descriptor. The instance needs a mutable `__dict__` for the documented wrapper properties and statistics.
Separate method keys by instance include-self-in-key
from cachetools import LRUCache, cachedmethod
from cachetools.keys import hashkey
shared = LRUCache(maxsize=1_000)
class Client:
@cachedmethod(lambda self: shared, key=lambda self, path: hashkey(id(self), path))
def get(self, path: str):
return self.request(path)The default `methodkey` ignores `self`. Add stable instance identity when several objects share one cache and their results differ.
Read memoization statistics inspect-hit-rate
from cachetools import LRUCache, cached
@cached(LRUCache(maxsize=64), info=True)
def normalize(value: str):
return value.casefold().strip()
print(normalize.cache_info())Statistics are enabled when the wrapper is created. In cachetools 7, pass `info` by keyword rather than as a fourth positional argument.
Advance TTL without sleeping test-expiration
from cachetools import TTLCache
now = [0.0]
cache = TTLCache(maxsize=10, ttl=30, timer=lambda: now[0])
cache['token'] = 'abc'
now[0] = 31.0
assert 'token' not in cacheA custom timer only needs values that support `timer() + ttl` and later comparison. This example crosses the 30-second deadline deterministically.
Choose expiry from each value set-per-item-expiry
from cachetools import TLRUCache
def time_to_use(key, value, now):
return now + value['ttl_seconds']
cache = TLRUCache(maxsize=100, ttu=time_to_use)
cache['fast'] = {'ttl_seconds': 5, 'payload': 'x'}`ttu(key, value, timer())` must return an expiry comparable with future timer values. Version 7.1.5 fixed stale values surviving expired overwrites.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| cacheout | PyPI | Use it when a small in-memory cache with defaults, callbacks, and bulk operations matters more than choosing among several eviction algorithms. |
| dogpile.cache | PyPI | Use it when cache regions, backend plugins, and distributed stores such as Redis or Memcached belong in the same caching API. |
| asyncache | PyPI | Use it with cachetools cache classes when the memoized call is an async function. |
More utils guides
lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

