mrkeyoor.com_
Thu 06 Aug 01:00 UTC
PyPIUtilsupdated 05 Aug 2026

cachetools

cachetools gives you in-memory caches as plain Python mappings plus decorators to memoize functions with them. Where functools.lru_cache locks you into one eviction policy and no expiry, cachetools ships LRUCache, LFUCache, FIFOCache, RRCache, TTLCache (time-to-live), and TLRUCache (per-item expiry), all dict-like objects with a fixed maximum size. The @cached and @cachedmethod decorators wrap any function or method with any of these caches, support custom key functions, optional locks for thread safety, and a condition variable to stop identical calls from running concurrently. It is pure Python with zero dependencies.

Verdict

The standard answer once functools.lru_cache stops being enough, which usually means the moment you need TTL. Small, stable, and boring in the best way; just remember it is single-process and thread safety is on you.

API stability5/5The cache classes and decorator API have been recognizably the same for a decade; the v7 breaking changes were a Python 3.10 floor and dropping one positional parameter, both trivial migrations.
Docs4/5cachetools.readthedocs.io documents every class, decorator, and the locking and condition semantics with runnable examples; the README itself is thin, pointing you at the docs.
Maintenance4/5Pushed August 2026 with steady patch releases and a nearly empty tracker (1 open issue or PR), but it is essentially a single-maintainer project (tkem), which is the main long-term risk.
Ecosystem4/5About 81M weekly downloads and a dependency of foundational packages like google-auth; the extension ecosystem is small but real (asyncache, shelved-cache, CacheToolsUtils are listed in the README).

Use it if

  • You need memoization with expiry: @cached(TTLCache(maxsize=1024, ttl=600)) is the one-liner functools cannot do
  • You want the cache as a first-class object you can inspect, iterate, pop entries from, or share between functions, instead of the sealed box lru_cache gives you
  • You need to cache methods with per-instance caches, which functools.lru_cache famously gets wrong by keeping self alive forever
  • You want to bound a cache by memory-ish size with getsizeof instead of entry count, or need per-item TTLs via TLRUCache
Skip it if

Setup reality

pip install cachetools is as easy as it gets: pure Python, zero dependencies, type stubs bundled since 7.1. The real work is conceptual. Thread safety is opt-in, so you must pass lock=threading.Lock() yourself and remember the lock only guards the cache, not your function. The cache object in @cached is created once at import time, so putting TTLCache(...) inline is fine but sharing it across functions needs distinct key functions or entries collide. @cachedmethod takes a callable that extracts the cache from self, which reads oddly the first time, and class-level caches keep instances alive unless you use per-instance ones in __init__. v7 dropped Python 3.9 and the positional info argument to @cached.

Patterns

Cache results for ten minutesmemoize-with-ttl

from cachetools import cached, TTLCache

@cached(cache=TTLCache(maxsize=1024, ttl=600))
def get_weather(place):
    return fetch_weather_api(place)

Expired entries are evicted lazily on access, not by a background thread, so memory frees up when the cache is touched, not when the TTL passes.

Use a cache directly as a dictlru-cache-as-mapping

from cachetools import LRUCache

cache = LRUCache(maxsize=32)
cache['a'] = 1
cache['b'] = 2

print(cache.get('a'))      # 1, and 'a' is now most recently used
print(len(cache), cache.maxsize)  # 2 32
cache.pop('b', None)

All cache classes are MutableMappings: get, pop, in, iteration, and items() work. Adding beyond maxsize evicts per the class's algorithm instead of raising.

Make a cached function thread-safethread-safe-cache

import threading
from cachetools import cached, TTLCache

@cached(cache=TTLCache(maxsize=500, ttl=300), lock=threading.Lock())
def lookup(user_id):
    return db_query(user_id)

The lock only guards cache reads and writes. The wrapped function runs outside the lock, so two threads can still compute the same missing key at once.

Stop identical calls running concurrentlyprevent-cache-stampede

import threading
from cachetools import cached, TTLCache

@cached(cache=TTLCache(maxsize=100, ttl=60),
        condition=threading.Condition())
def expensive(query):
    return run_report(query)

With condition=, a thread that finds an identical call in flight waits for it and returns the cached result, instead of duplicating the work. Costs some overhead; use it for genuinely expensive calls.

Cache a method with a per-instance cachecache-method-per-instance

import operator
from cachetools import cachedmethod, LRUCache

class PepStore:
    def __init__(self):
        self.cache = LRUCache(maxsize=32)

    @cachedmethod(operator.attrgetter('cache'))
    def get_pep(self, num):
        return fetch_pep(num)

cachedmethod takes a function of self that returns the cache. Creating the cache in __init__ keeps entries per instance and lets instances be garbage collected, unlike lru_cache on a method.

Share one cache across functions safelyshare-cache-between-functions

from functools import partial
from cachetools import cached
from cachetools.keys import hashkey

numcache = {}

@cached(numcache, key=partial(hashkey, 'fib'))
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

@cached(numcache, key=partial(hashkey, 'luc'))
def luc(n):
    return 2 - n if n < 2 else luc(n - 1) + luc(n - 2)

Without distinct key prefixes, fib(10) and luc(10) would collide on the same key and return each other's results. A plain dict is a valid unbounded cache.

Ignore an argument in the cache keycustom-cache-key

from cachetools import cached, LRUCache
from cachetools.keys import hashkey

@cached(LRUCache(maxsize=256), key=lambda session, user_id: hashkey(user_id))
def load_user(session, user_id):
    return session.query(User).get(user_id)

The default hashkey includes every argument, so unhashable or irrelevant ones (db sessions, loggers) break caching or fragment it. The key function receives the same arguments as the wrapped function.

Clear the cache or evict one entryinvalidate-cache-entries

import threading
from cachetools import cached, TTLCache

@cached(cache=TTLCache(maxsize=100, ttl=3600), lock=threading.Lock())
def get_config(env):
    return load_config(env)

# drop everything (handles locking for you)
get_config.cache_clear()

# drop one entry
key = get_config.cache_key('prod')
with get_config.cache_lock:
    get_config.cache.pop(key, None)

The decorator exposes cache, cache_key, cache_lock, and cache_condition on the wrapper. cache_clear() locks for you; manual pops need the with block yourself.

Measure cache effectiveness with cache_infomeasure-hit-rate

from cachetools import cached, LRUCache

@cached(cache=LRUCache(maxsize=32), info=True)
def get_pep(num):
    return fetch_pep(num)

for n in (8, 290, 308, 8, 218, 8):
    get_pep(n)

print(get_pep.cache_info())
# CacheInfo(hits=2, misses=4, maxsize=32, currsize=4)

info=True must be passed as a keyword since v7 and adds a small per-call cost, so it is off by default. Handy to verify a cache is actually earning its memory.

Give each entry its own lifetimeper-item-ttl

from cachetools import TLRUCache

def my_ttu(_key, token, now):
    # expire each token when the API says it expires
    return now + token['expires_in']

cache = TLRUCache(maxsize=100, ttu=my_ttu)
cache['svc-a'] = {'access_token': 'abc', 'expires_in': 3600}

The ttu function returns the expiration time for each item, based on key, value, and the current time. Uses time.monotonic by default, so return now + seconds, not a wall-clock timestamp.

Bound the cache by content size, not entry countsize-aware-cache

import threading
import urllib.request
from cachetools import cached, LRUCache

@cached(cache=LRUCache(maxsize=640 * 1024, getsizeof=len),
        lock=threading.Lock())
def get_page(url):
    with urllib.request.urlopen(url) as s:
        return s.read()

With getsizeof, maxsize is the sum of item sizes instead of a count. An item larger than maxsize raises ValueError rather than silently evicting everything.

Drop-in lru_cache replacement with TTLstdlib-compatible-decorators

import cachetools.func

@cachetools.func.ttl_cache(maxsize=128, ttl=600, typed=False)
def get_rates(currency):
    return fetch_rates(currency)

print(get_rates.cache_info())

cachetools.func mirrors the functools.lru_cache interface (cache_info, cache_clear, typed) with fifo_cache, lru_cache, rr_cache, and ttl_cache variants, easing migration in either direction.

Alternatives

PackageRegistryPick it when
diskcachePyPIYou want the cache to survive restarts and be shared across processes via SQLite on disk.
aiocachePyPIYou are caching async functions and may want Redis or Memcached backends behind one API.
asyncachePyPIYou want cachetools-style decorators that work on coroutines with the same cache classes.