How to Implement a Temporary Block in Python

Build a reusable PenaltyBox class that temporarily blocks access after a failure and reports remaining lockout time.

Easy Python 3.9+ Aug 9, 2026 Reliability & rate limiting 14 views 0 copies

Python code

36 lines
Python 3.9+
class PenaltyBox:
    def __init__(self, block_seconds: int = 30):
        self.block_seconds = block_seconds
        self._blocked_until = 0.0
        self._attempts = 0

    def try_access(self, current_time: float) -> bool:
        if self._blocked_until and current_time < self._blocked_until:
            return False
        return True

    def record_failure(self, current_time: float) -> None:
        self._attempts += 1
        self._blocked_until = current_time + self.block_seconds

    def remaining_seconds(self, current_time: float) -> float:
        if self._blocked_until and current_time < self._blocked_until:
            return round(self._blocked_until - current_time, 2)
        return 0.0


if __name__ == "__main__":
    mock_time = 1000.0
    box = PenaltyBox(block_seconds=10)

    box.record_failure(mock_time)
    print(f"Immediately after failure — allowed: {box.try_access(mock_time)}")
    print(f"Remaining: {box.remaining_seconds(mock_time)}s")

    mock_time += 5
    print(f"5s later — allowed: {box.try_access(mock_time)}")
    print(f"Remaining: {box.remaining_seconds(mock_time)}s")

    mock_time += 6
    print(f"11s later — allowed: {box.try_access(mock_time)}")
    print(f"Remaining: {box.remaining_seconds(mock_time)}s")

Output

stdout
Immediately after failure — allowed: False
Remaining: 10.0s
5s later — allowed: False
Remaining: 5.0s
11s later — allowed: True
Remaining: 0.0s

How it works

The PenaltyBox tracks a _blocked_until timestamp (in seconds) and increments _attempts on each failure. try_access compares the current time against _blocked_until to decide if access is allowed — returning False while blocked and True once the lockout expires. remaining_seconds computes the difference between the lockout deadline and now, rounded to two decimals, or zero when no block is active. The class accepts an injected block_seconds duration and uses a monotonic-style current_time parameter, so tests and real code can both feed the same clock source. Because the state is just two floats, the box is trivially serializable and easy to reason about under concurrency.

Common mistakes

  • Using `time.time()` directly inside the class, which makes unit tests flaky without mocking
  • Forgetting to reset `_blocked_until` to 0 after the lockout expires, causing stale checks
  • Relying on wall-clock time instead of an injectable current_time for deterministic behavior

Variations

  1. Add a `reset()` method to clear `_attempts` and `_blocked_until` manually
  2. Use a `functools.lru_cache` or Redis-backed store for distributed locking

Real-world use cases

  • Lock a user out of a login form for 30 seconds after five failed password attempts.
  • Rate-limit webhook retries by blocking a client for a fixed window after repeated 429 responses.
  • Pause a background worker after consecutive API auth failures, then automatically resume.

Sponsored

Run this sample

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

Open editor

More from Reliability & rate limiting

Related tutorials and quizzes for this topic.