We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Stdlib Mastery step 29 of 55
partial: a signature-preserving retrier, and the 3.14 descriptor change
Write a retry wrapper that keeps the wrapped function’s signature, and a class that exposes pre-configured variants as attributes that bind correctly on 3.13 and 3.14+ alike.
def make_retrier(
fn: Callable[P, R], *, attempts: int, delay: float,
sleep: Callable[[float], None],
) -> Callable[P, R]: ...
-
Calls
fn, retrying onValueError, up toattemptstimes total. -
Before attempt
i(1-indexed from the second), callssleep(delay * i)— so the waits aredelay*1, delay*2, .... The clock is injected; a function that callstime.sleepdirectly cannot be tested. -
Re-raises the last
ValueErrorwhen every attempt fails. -
attempts < 1raisesValueErrorfrommake_retrieritself.
def shout(text: str, *, mark: str = "!") -> str: ... # text.upper() + mark
class Notifier:
def __init__(self, channel: str) -> None: ...
def send(self, body: str, *, level: str = "info") -> str: ...
# f"{self.channel}/{level}: {body}"
alert = ... # send with level="alert", still receives self
banner = ... # shout with mark="!!!", must NOT receive self
def solve(fail_times: int, attempts: int, boost: int | None) -> dict[str, object]:
solve builds a closure flaky(base: int, *, boost: int = 1) -> int that
raises ValueError on its first fail_times calls and then returns
base * boost, wraps it with make_retrier(..., delay=0.25, sleep=waits.append),
and calls it as retrier(10) or retrier(10, boost=boost). It returns:
{"outcome": object, # the result, or "exhausted", or "ValueError" if
# make_retrier itself rejected `attempts`
"calls": int, "waits": list[float],
"alert": str, # notifier.alert("disk full") -> "ops/alert: disk full"
"banner": str, # notifier.banner("ship it") -> "SHIP IT!!!"
"partial_pickles": bool, # a round-tripped partial still works
"lambda_pickles": bool} # pickling a lambda -> False
When make_retrier raises, return
{"outcome": "ValueError", "calls": 0, "waits": [], "alert": "", "banner": "", "partial_pickles": False, "lambda_pickles": False}.
Use Notifier("ops"). For the pickle checks, round-trip
partial(str.upper, "revived") and confirm calling it gives "REVIVED"; and
attempt pickle.dumps on a lambda inside a try.
The 3.14 breaking change. functools.partial became a method
descriptor in 3.14, so a bare partial stored as a class attribute now
binds self when accessed through an instance. Before 3.14 it did not; 3.13
emitted a FutureWarning. banner therefore needs staticmethod — the
documented fix for preserving the old behaviour — and alert needs
partialmethod, which has bound self correctly on every version. A bare
partial in a class body means one thing on 3.13 and the other on 3.14, and
the difference surfaces as a TypeError about argument counts whose traceback
names the wrapped function rather than the class.
Why picklability matters. multiprocessing and
ProcessPoolExecutor have to ship your callable across a process boundary. A
partial of a module-level function pickles; a lambda raises. That single
difference decides most real choices between them.
Your submission must pass mypy --strict, and Callable[..., Any] will not
do: it erases every call site of the retried function and turns the return
into Any, which propagates. Use ParamSpec. (mypy has type-checked partial
application since 1.11, so “partial is untypeable” is stale advice.)
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.