Skip to content

← Precise Type Hints with Literals and Generics step 6 of 26

Easy Primitives

Lock Down a Rate Limiter with Final and @final

An API gives each caller a small, fixed number of requests. Admins get 5, guests get 1, and every other caller gets 3. You build the limiter, and you add type hints so that mypy stops a teammate from changing its settings or subclassing it by mistake.

assert solve(["a", "guest", "a", "guest", "a", "a"]) == [
    True, True, True, False, True, False,
]

"a" gets three requests and "guest" gets one. After that, each request returns False.

Three annotations that stop changes

Final, from an earlier exercise in this track, means mypy rejects any later assignment to the name. ClassVar marks an attribute of the class itself, shared by every instance. The next exercise covers it in depth.

Annotation What mypy rejects
MAX_BURST: Final[int] = 3 Any later assignment to MAX_BURST, in this class or a subclass
LIMITS: ClassVar[dict[str, int]] Assigning self.LIMITS = ... through an instance
@final above the class Any subclass of RateLimiter

mypy checks these rules, and Python does not enforce them at runtime. ClassVar also leaves the dictionary itself open to change, so self.LIMITS["guest"] = 9 passes mypy. Leave LIMITS unchanged.

What to write

from typing import ClassVar, Final, final

@final
class RateLimiter:
    MAX_BURST: Final[int] = 3
    LIMITS: ClassVar[dict[str, int]] = {"admin": 5, "guest": 1}

    def __init__(self) -> None: ...
    def limit_for(self, key: str) -> int: ...
    def allow(self, key: str) -> bool: ...

def solve(keys: list[str]) -> list[bool]: ...
  • Use all three annotations as shown.
  • limit_for returns the key's entry in LIMITS, or MAX_BURST for any other key. Keys are case-sensitive, so "Admin" and "" both get 3.
  • __init__ creates an empty dictionary of counts on the instance.
  • allow reads the key's count, starting at 0. If the count has reached the key's limit, it returns False and leaves the count as it is. Otherwise it adds 1 to the count and returns True.
  • Each key has its own count, and so does each limiter.
  • solve creates one new RateLimiter, calls allow for each key in order, and returns the results. An empty list returns [].

The limit is a fixed quota per limiter. There is no clock, refill or time window.

Keep the counts on the instance

Create the counts dictionary in __init__, not in the class body. A dictionary in the class body is one object that every limiter shares. The grader loads your code once and runs a batch of test cases against it. Shared counts would carry over from one case to the next:

assert solve(["guest", "guest"]) == [True, False]
assert solve(["guest", "guest"]) == [True, False]  # fails if counts are shared

Going further