We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Structural Typing and the Hard Parts step 11 of 24
ParamSpec and Concatenate: decorators that keep the signature
One Callable[..., Any] decorator silently disables type checking for every
call site of every function it wraps. No other typing mistake has a blast
radius that large — a single @retry on fifty handlers erases the checked
signature of fifty functions and hundreds of call sites, and --strict
reports nothing, because the annotation is complete.
The shape
def retry[**P, R](fn: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(fn)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: ...
return wrapper
Two rules the checker enforces: P.args and P.kwargs must appear
together, and only as the annotations of *args and **kwargs. You
cannot use them anywhere else — they are not real types, they are a
placeholder for “whatever this function’s parameter list was”.
Concatenate[Req, P] prepends positional-only parameters. A decorator
annotated
def with_session[**P, R](fn: Callable[Concatenate[Session, P], R]) -> Callable[P, R]:
consumes the leading Session argument. The wrapped function declares it;
the public signature no longer has it; and a caller who tries to pass one is
a static error.
Its documented limits
PEP 612 is explicit that it does not cover everything:
-
arity reduction (
functools.partial-style: bind an argument and shorten the signature) is not expressible; - there is no syntax to prepend a keyword-only parameter — Concatenate is positional-only.
Knowing those two saves days of trying.
Also note --disallow-untyped-decorators, which is part of --strict: an
untyped decorator applied to a typed function is itself an error, because
it silently returns Any.
Your task
Implement retry and with_session with the signatures above.
retry calls the function up to ATTEMPTS (3) times, swallowing
RuntimeError; if all attempts fail it raises
RuntimeError(f"gave up after {ATTEMPTS} attempts") chained from the last
exception.
with_session constructs a fresh Session() per call and passes it as the
first positional argument.
def solve(failures: list[int], labels: list[str], queries: list[str]) -> list[str]:
For each (fail, label) pair, call flaky([fail + 1], label) inside a
try and append either its result or str(exc). Then for each query,
append run_query(query) and run_query(query, upper=True) — with no
session argument, which is the whole point of Concatenate.
flaky decrements the single-element budget list it is handed and raises
while it is still positive, so fail is literally the number of failures
before success. State lives in the argument, not in the module — which is
also how you make a retry policy testable in real code.
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.