mrkeyoor.com_
Sun 20 Sept 04:56 UTC
PyPIUtilsupdated 19 Sept 2026

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.

Verdict

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

Lab card: what happened when we installed cachetoolsScreenshot of cachetools documentation
Install✓ · 0.2s1 package on disk · 1 MB
Importimport cachetools in 0.05s · pure Python · py.typed · requires Python >=3.10
Known vulns0(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.

API stability4/5cachetools 7 keeps the mapping classes, decorators, key helpers, and cache inspection attributes familiar from earlier releases, while version 7.1.7 only adjusts larger-value replacement behavior. The 7.0 boundary did remove Python releases below 3.10, stopped accepting `info` as the fourth positional `cached` argument, and changed `cachedmethod` wrappers into descriptors. Those are contained changes, but class-method and slot-only-instance users must read the migration notes.
Docs5/5The 7.1.7 reference names each eviction rule, constructor, property, decorator parameter, and key helper. It directly warns that cache classes are not thread-safe, explains insertion-time sizing and delayed TTL cleanup, and shows how locks and conditions differ during memoization. The examples also cover custom timers, per-item TLRU deadlines, shared caches, method caches, and cache statistics, leaving few behavioral traps undocumented.
Maintenance4/5The repository is unarchived, was pushed on August 1, 2026, and GitHub reports 2 open issues and pull requests plus 2,776 stars. Releases 7.1.0 through 7.1.7 arrived between May and August 2026, adding type stubs and correcting TLRU expiry, negative size values, and replacement accounting. The project is maintained by a small surface rather than a large organization, but current fixes track specific cache invariants instead of cosmetic churn alone.
Ecosystem4/5The stored registry count is 74,205,668 weekly downloads, and the package has no runtime dependencies in our install. cachetools works with ordinary mutable mappings and exposes hooks used by asyncache and other extensions, so it is easy to place below framework code. Its scope stops at one process: it has no Redis protocol, persistence, serializer, invalidation bus, or framework configuration layer, which limits how far that ecosystem carries into distributed deployments.

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.
Skip it if

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_bytes

The 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 cache

A 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

PackageRegistryPick it when
cacheoutPyPIUse it when a small in-memory cache with defaults, callbacks, and bulk operations matters more than choosing among several eviction algorithms.
dogpile.cachePyPIUse it when cache regions, backend plugins, and distributed stores such as Redis or Memcached belong in the same caching API.
asyncachePyPIUse 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.