We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Structural Typing and the Hard Parts step 2 of 24
The protocol attribute invariance trap
This is the single most common reason a reviewer rejects a Protocol, and
the two usual “fixes” are both wrong: widening the implementation until it
matches, or reaching for cast.
The rule
A protocol member declared as a plain attribute is readable and writable.
A caller holding a HasPayload is allowed to do envelope.payload = {}.
For that to be sound, the attribute type must be invariant — the
implementation’s type must match exactly, not merely be a subtype.
So this fails:
class HasPayload(Protocol):
payload: Mapping[str, object] # mutable member -> invariant
@dataclass(frozen=True)
class JsonEnvelope:
payload: dict[str, str] # a *subtype*, and therefore rejected
The reasoning is the same one that makes list[Dog] not a list[Animal].
If JsonEnvelope were accepted, a function taking HasPayload could assign
a dict[str, int] into a field the rest of the program believes holds
dict[str, str].
The fix
Declare the member read-only, as a property:
class HasPayload(Protocol):
@property
def payload(self) -> Mapping[str, object]: ...
A read-only member is covariant, so every implementation whose payload type is a subtype now satisfies it. Crucially, a plain attribute satisfies a read-only property member — the implementation does not have to change at all, and does not have to become a property. (The reverse is not true: a read-only property does not satisfy a mutable attribute member.)
Your task
The file you are given already works at runtime and already has three implementations you must not touch:
-
JsonEnvelope— supplies the narrowerdict[str, str] -
FrozenEnvelope— supplies exactlyMapping[str, object] -
LazyEnvelope— supplies the member as a real@property
Change only the protocol so all three type-check.
def solve(mutable: list[dict[str, str]], readonly: list[dict[str, str]]) -> list[str]:
Build one JsonEnvelope per entry of mutable, then one FrozenEnvelope
and one LazyEnvelope per entry of readonly (in that order), and
return describe(...) of each. describe renders "<count>:<sorted keys joined by commas>".
The trade you are making
Making a member read-only is a real narrowing of the interface: consumers of
HasPayload can no longer assign to payload, and mypy will tell them so.
That is the point. If a consumer genuinely needs to write through the
protocol, the invariance is correct and the implementations must widen — but
that is a decision you make deliberately, not one you discover when the
checker complains.
Loading visualization…
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.