Implement a TTL cache with a mock clock in Python

This code creates a simple TTL cache that stores values with an expiration timestamp and allows injecting a mock time function to test expiry behavior deterministically.

Medium Python 3.9+ Aug 9, 2026 Caching & Redis 15 views 0 copies

Python code

40 lines
Python 3.9+
import time
from functools import wraps

class TTLCache:
    def __init__(self, ttl_seconds):
        self.ttl = ttl_seconds
        self.cache = {}
        self._now = time.time

    def set_mock_time(self, mock_time_fn):
        """Inject a mock time function for testing TTL expiry."""
        self._now = mock_time_fn

    def get(self, key, default=None):
        now = self._now()
        if key in self.cache:
            value, expires_at = self.cache[key]
            if now < expires_at:
                return value
            else:
                del self.cache[key]  # Expired, remove
        return default

    def set(self, key, value):
        self.cache[key] = (value, self._now() + self.ttl)

if __name__ == "__main__":
    # Simulate time with a mutable clock
    fake_clock = {"time": 1000.0}
    cache = TTLCache(ttl_seconds=5)
    cache.set_mock_time(lambda: fake_clock["time"])

    cache.set("user", {"name": "Alice"})
    print("Initial get:", cache.get("user"))

    fake_clock["time"] += 3  # 3 seconds later — still valid
    print("After 3s:", cache.get("user"))

    fake_clock["time"] += 3  # 6 seconds total — expired
    print("After 6s:", cache.get("user", "MISS"))

Output

stdout
Initial get: {'name': 'Alice'}
After 3s: {'name': 'Alice'}
After 6s: MISS

How it works

The TTLCache stores each value as a tuple containing the actual value and an expiration timestamp computed at set time. The get method first checks if the key exists, then compares the current time (obtained from the injected time function) against the expiration; if expired, it deletes the entry and returns the default. Injection of a mock time function (set_mock_time) allows testing TTL logic without sleeping or depending on the real system clock. This pattern keeps the cache logic pure and testable, which is especially useful in unit tests for caching layers.

Common mistakes

  • Forgetting to use the injected time function consistently in both `set` and `get`
  • Not handling expired entries by deleting them, causing stale data to remain
  • Assuming the default `time.time` is used correctly without mocking in tests

Variations

  1. Use a monotonic clock like `time.monotonic` instead of wall-clock time to avoid drift
  2. Implement automatic background cleanup of expired entries with a background thread

Real-world use cases

  • Unit testing caching layers in web applications without waiting for real time to pass.
  • Simulating cache expiration in integration tests to verify fallback behavior when data is refreshed.
  • Testing time-based features like session expiry or temporary authorization tokens deterministically.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Caching & Redis

Related tutorials and quizzes for this topic.