Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Python TTL cache: expire at a defined boundary and use an injected clock

Last updated: 30 Sept 20264 min read
tutorial
IntermediateBy AITrove Editorial

A TTL cache makes a stored value eligible only until its recorded expiry time, independently of its eviction order.

Download Python source kit

Operation contract

The cache stores at most four scalar receipt values. put validates its key, amount, TTL and clock before changing state. Expired entries are removed before capacity eviction; an overwrite receives a new expiry. get treats now equal to expiry as a miss and does not extend the TTL. The injected clock makes tests deterministic and allows the cache to reject clock reversal. A returned scalar has no mutable alias for the caller to modify.

Failure and ownership boundary

TTL is freshness policy, not durable state or authorization. No background thread removes idle expired records; they remain until the next operation, still bounded by capacity. Python LRU cache project: make misses, replacement and eviction explicit, Python lru_cache: bound retention and include the revision in the key and Python expiry exercise: separate pending, active and expired records at exact boundaries separate expiry from caching and retry decisions.

Working program

python
from collections import OrderedDict

class ReceiptTTL:
    def __init__(self, clock):
        self.clock, self.last = clock, -1
        self.entries = OrderedDict()
    def timestamp(self):
        now = self.clock()
        if type(now) is not int or not 0 <= now <= 1000000 or now < self.last:
            raise ValueError("bounded nondecreasing clock required")
        return now
    def expire(self, now):
        for key in [key for key, (_, until) in self.entries.items() if until <= now]:
            del self.entries[key]
        self.last = now
    def put(self, key, amount, ttl):
        if type(key) is not str or not key.isascii() or not key.isalnum() or not 1 <= len(key) <= 16 or type(amount) is not int or not 0 <= amount <= 1000000 or type(ttl) is not int or not 1 <= ttl <= 100:
            raise ValueError("bounded receipt fields required")
        now = self.timestamp()
        self.expire(now)
        self.entries[key] = (amount, now + ttl)
        self.entries.move_to_end(key)
        if len(self.entries) > 4:
            self.entries.popitem(last=False)
    def get(self, key):
        if type(key) is not str or not key.isascii() or not key.isalnum() or not 1 <= len(key) <= 16:
            raise ValueError("bounded key required")
        now = self.timestamp()
        self.expire(now)
        record = self.entries.get(key)
        return None if record is None else record[0]

ticks = [100]
cache = ReceiptTTL(lambda: ticks[0])
cache.put("R41", 125, 10)
ticks[0] = 109
print("before expiry:", cache.get("R41"))
ticks[0] = 110
print("at expiry:", cache.get("R41"))
cache.put("R41", 250, 5)
ticks[0] = 109
try:
    cache.get("R41")
except ValueError:
    print("clock reversal rejected")
print("retained:", list(cache.entries))

Output

Output
before expiry: 125
at expiry: None
clock reversal rejected
retained: ['R41']

Costs and limits

Expiry scanning is O(C) per operation for capacity C, with O(C) storage. The accepted capacity is fixed at four, so this is intentionally simpler than a heap of deadlines. Clock callbacks must be owned, side-effect-controlled code; a lock is still required if multiple threads share this cache.

Common Mistakes

  • Specify whether now equal to expiry is accepted.
  • Do not confuse write-order eviction with access-order LRU.

Connected lessons

Python LRU cache project: make misses, replacement and eviction explicit, Python OrderedDict: explicit reordering differs from insertion order, Python expiry exercise: separate pending, active and expired records at exact boundaries.

python
ttl-cache-project
Storage details