Skip to content

← Concurrency Without Regret step 13 of 38

Medium Primitives

A Fetcher Protocol three implementations can satisfy

Declare a Protocol that three different implementations can satisfy — then consume it.

class Fetcher[T](Protocol):
    def fetch(self, key: str) -> ???: ...

async def collect[T](fetcher: Fetcher[T], keys: Sequence[str]) -> dict[str, T]:

collect fetches every distinct key exactly once, in first-seen order, and returns the mapping. Duplicates in keys must not produce a second call — the report’s calls log is what proves it.

The real exercise is one line

The starter declares the Protocol as

async def fetch(self, key: str) -> T: ...

and it does not type-check, because the driver’s registry is annotated dict[str, Fetcher[bytes]] and contains three implementations:

  • AsyncFetcher — a coroutine function. Fine either way.
  • FutureFetcher — a plain def that resolves a Future immediately. This is what a cache looks like: no suspension when the answer is already known.
  • TaskFetcher — a plain def that schedules the work and returns the Task.

An async def in a Protocol demands that every implementation be a coroutine function, forever. A plain method returning an Awaitable[T] demands only that what comes back can be awaited — which a coroutine object, a Future and a Task all can.

That is not a stylistic preference. It is the difference between a plug-in point that can later grow a cache or a request batcher, and one that cannot without a breaking change to your published interface.

What the report proves

  • payloads — the awaited values, as bytes. If you forget the await, what lands in the dict is a coroutine object or a Future, and the comparison fails loudly. (--strict alone would not have told you: the unused-awaitable error code is opt-in.)
  • calls — one entry per fetch call, in order. Dedup happens before the call, not after.
  • order — the mapping’s key order is first-seen order.

Where the type system earns its keep

Fetcher[T] is generic in what it produces, so collect returns dict[str, T] rather than dict[str, Any], and a Fetcher[bytes] gives you dict[str, bytes] all the way to the caller. Under --disallow-any-generics a bare Awaitable is rejected — parameterise it — and under --strict the accumulator needs its own annotation, because an empty dict has no inferable value type.

Loading visualization…