You call label(text, mark="!!!") in a dozen places, always with the same
mark. The functools.partial function makes a new callable with that argument
already filled in:
from functools import partial
def label(text: str, *, mark: str = "!") -> str:
return text.upper() + mark
banner = partial(label, mark="!!!")
print(banner("ready")) # READY!!!
print(banner("ready", mark="?")) # READY?
print(banner.func is label, banner.keywords) # True {'mark': '!!!'}
Calling banner("ready") calls label("ready", mark="!!!"). A keyword you
pass at call time replaces the stored one, and stored positional arguments go
before the ones you pass. The partial keeps what it stored in .func, .args
and .keywords, so you can inspect it when you debug.
mypy checks the arguments you give partial, too. For example,
partial(label, mark=3) is an error, because mark must be a str.
A partial can be pickled
Pickling turns an object into bytes, so that you can save it to a file or send it to another process. The parallelism track uses it to send work to other processes. A partial pickles if its function and its stored arguments do:
import pickle
from functools import partial
upper = partial(str.upper, "revived")
restored = pickle.loads(pickle.dumps(upper))
print(restored()) # REVIVED
Python pickles a function by its name, so that the process that loads it can import it again. A lambda has no name to import, so pickling one fails. Where work may go to another process, prefer a named function or a partial to a lambda. The exercise checks both cases.
Put presets on a class
When you store a partial on a class, say whether it should receive self:
from functools import partial, partialmethod
def shout(text: str, *, mark: str = "!") -> str:
return text.upper() + mark
class Notifier:
def __init__(self, channel: str) -> None:
self.channel = channel
def send(self, body: str, *, level: str = "info") -> str:
return f"{self.channel}/{level}: {body}"
alert = partialmethod(send, level="alert")
banner = staticmethod(partial(shout, mark="!!!"))
notifier = Notifier("ops")
print(notifier.alert("disk full")) # ops/alert: disk full
print(notifier.banner("ship it")) # SHIP IT!!!
print(Notifier.banner("ship it")) # SHIP IT!!!
- A
partialmethodmakes a method. Like any method,alertreceives the instance asself, then the presetlevel. - A
staticmethodaround a partial stops Python from passingself, sobannerreceives only the caller's arguments, through an instance or the class.
The wrappers also protect you from a change in Python 3.14. Before 3.14, a bare
partial on a class never received self. From 3.14, it receives self when
you call it through an instance, like a method. So in this exercise's starter,
the bare alert = partial(send, level="alert") raises TypeError on Python
3.12 and works on 3.14, and the bare banner does the opposite. Wrapped in
partialmethod or staticmethod, both behave the same on every version.
Keep a retry wrapper's types
The exercise asks for make_retrier, which wraps a function and calls it again
after a failure. It uses the pattern from "Write Typed Decorators with wraps
and ParamSpec". The inner function takes *args: P.args, **kwargs: P.kwargs,
and make_retrier returns Callable[P, R]. So mypy checks every call to the
retrier against the original function's parameters and return type. The
starter's Callable[..., Any] lets any call through and makes every result
Any.
The retrier also takes a sleep function instead of calling time.sleep
itself.
Check your understanding
Why does make_retrier take a sleep function instead of calling time.sleep?
Show answer
Hide answer
So that a test can pass waits.append. It records each delay and returns at
once, so the test checks the delays without waiting for them. Real code passes
time.sleep. Retry only errors that may go away on a second try, such as a
timeout, and do not sleep after the last attempt. Retrying also repeats side
effects, so a retried call that sends an email can send it twice.
Going further
Python 3.14 added functools.Placeholder, which leaves a gap among the stored
positional arguments for the caller to fill. The exercise supports Python 3.12,
so it does not use it.