We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← The Type System as a Design Tool step 6 of 24
A @final rate limiter
Two different words, one keyword, and a decision you are making about your callers whether you mean to or not.
Final on a name says “this is never rebound”. On a module constant it is
documentation the checker enforces. On a class attribute it also means “no
subclass may override this”.
@final on a class says “do not subclass me”. That has two consequences.
The social one: you are telling callers you will not maintain subclass
compatibility, so you are free to rename private methods and change the MRO.
The technical one, which is the more interesting: it lets a checker conclude
that type(x) is C implies x is exactly a C, which unlocks narrowing that
is otherwise unsound — with an open class, type(x) is C could still be a
subclass instance with different behaviour.
@final on a method says “override this and it is an error”, which is how
you protect an invariant that a template method depends on.
A correction worth carrying, because PEP 591 originally forbade it: Python
3.13 relaxed the Final/ClassVar combination. ClassVar[Final[int]] is now
legal. Before that you had to choose between “shared across instances” and
“never rebound” in a single annotation.
The task
@final
class RateLimiter:
MAX_BURST: Final[int] = 3
LIMITS: ClassVar[dict[str, int]] = {"admin": 5, "guest": 1}
def limit_for(self, key: str) -> int: ...
def allow(self, key: str) -> bool: ...
def solve(keys: list[str]) -> list[bool]: ...
limit_for returns the per-key override from LIMITS if there is one, and
MAX_BURST otherwise. allow returns True and records the use while that
key’s count is below its limit, and False forever after. Counts are
per-instance, kept in a private attribute created in __init__.
solve builds one RateLimiter and returns the result of allow for each
key in order.
Keys are exact and case-sensitive: "Admin" is not "admin" and gets the
default burst.
The state trap this problem is built around
The harness executes your module once and then calls the entrypoint for every test case in the same namespace. Any state you park on the class survives from one case to the next.
That is not an artefact of the harness — it is exactly what happens to a
long-lived process, and it is why “works in the test, fails on the second
request” is such a common bug. If you put the counters in a ClassVar dict and
never reset them, case two starts with case one’s traffic already counted, and
every run after the first gives a different answer.
Here LIMITS is a ClassVar because it is shared, read-only configuration.
The counters are per-instance because they are per-instance state. Getting that
split right is the whole design decision; Final and @final are how you write
it down so the checker holds you to it.
Types
--strict will not let you leave LIMITS unannotated. Note what each
annotation buys:
-
MAX_BURST: Final[int]— reassigning it anywhere, including in a subclass or through an instance, is a type error. -
LIMITS: ClassVar[dict[str, int]]—self.LIMITS = {}is amiscerror, because assigning to aClassVarthrough an instance is exactly the shadowing bug that makes shared state stop being shared. -
@finalon the class —class Stricter(RateLimiter)is a type error.
None of those three is enforced at runtime. All three are enforced in CI, which is where you want the argument to happen.
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.