Why define the sink interface as a Protocol inside your metrics scraper rather than an ABC every sink provider imports?
answer
- Ask who has to import whom
- The consumer states what it needs
- Retrofits classes you do not own
- One method, not a shared kitchen-sink base
- Shape is checked; behaviour still is not
basics
~20 sA Protocol declared in the scraper reverses the dependency: providers need no import and no inheritance, so pre-existing and standard-library objects fit, and each consumer states only the members it calls. An ABC would force every provider to depend on the scraper package.
solid answer
~50 sPut the `typing.Protocol` in the module that *consumes* the sink and it describes exactly what the scraper calls — say a single `write(line: str)`. Providers satisfy it by shape, so nothing imports the scraper, objects that already exist fit unchanged, and test doubles conform without a base class. That keeps the dependency arrow pointing at the consumer's requirement rather than at a shared base class every provider must inherit. Publishing an `abc.ABC` instead couples every provider to your package, forces inheritance, and cannot retrofit classes you do not own except through virtual registration. The trade you accept is that structural conformance is checked by a static tool and only describes shape: a sink whose `write` silently truncates long lines type-checks perfectly, so behaviour still belongs in documentation and conformance tests. Keep an ABC where you need runtime `isinstance` dispatch or shared implementation.
code
python · 20 linesfrom typing import Protocol
class LineSink(Protocol):
def write(self, line: str) -> None: ...
def scrape(sink: LineSink, samples: list[float]) -> None:
for value in samples:
sink.write(f"cpu={value:.2f}")
class TruncatingSink:
def __init__(self, limit: int) -> None:
self.limit = limit
self.lines: list[str] = []
def write(self, line: str) -> None:
self.lines.append(line[: self.limit])
sink = TruncatingSink(4)
scrape(sink, [0.42, 0.91])
print(sink.lines) # conforms structurally, truncates silentlygo deeper
Take away the direction of the import: with a Protocol the class that uses the object describes what it needs, and the object's author never has to import or inherit anything.
Explain what structural conformance buys — no inheritance, retrofitting existing classes, trivially conformant test doubles — and that a static type checker, not the interpreter, performs the comparison.
Name the costs as well as the wins: no runtime enforcement, mismatches surfacing in the consumer's build rather than the provider's, and shape-only checking that misses a silently truncating implementation.
Own the policy — narrow consumer-side protocols as the default contract, an optional abstract base for shared implementation, and a conformance test suite where behaviour actually matters.
This is the question about **who owns the interface**, and it is where structural typing earns its keep. **The shape of the problem.** A scraper collects samples and hands each formatted line to a sink. Sinks are many and varied: one appends to a list in tests, one writes to a file object, one pushes onto a queue, one is a class from a library you did not write. The scraper only ever calls one method. **With a published abstract base class**, the interface lives in the scraper's package and every provider imports it and inherits from it. That means: an import edge from every provider to your package; providers you do not control cannot participate without a wrapper; and objects that already have a perfectly good `write` are excluded on a technicality. It also means the interface is *provider-facing* — one base class shared by all consumers, which tends to grow every member any consumer ever wanted. **With a Protocol declared in the consuming module**, the scraper says what it needs and nothing else. A provider is conformant by having the members. Four practical wins: 1. **No dependency edge.** Providers import nothing. The scraper depends on a shape it defined itself, so the arrow points inward toward the consumer, which is the language-level version of depending on an abstraction you own. 2. **Retrofitting works.** A pre-existing class, or a standard-library object with a compatible `write`, satisfies the protocol as-is. With an ABC you would need `abc.ABCMeta.register` — which asserts the relationship without checking it — or an adapter class. 3. **Narrow, per-consumer contracts.** The scraper's protocol lists the one method it calls. A different consumer declares its own. Nobody is forced to implement members they do not need, and adding a member to one consumer's protocol does not touch the others. 4. **Frictionless test doubles.** A three-line class with the right method conforms; no base class, no registration, no mock library needed to satisfy the type. **Now the honest costs**, because a senior answer is the one that names them. **Structural conformance is shape, not behaviour.** A sink whose `write` accepts a `str` and stores only the first four characters conforms perfectly. The scraper emits `cpu=0.42`, the sink records `cpu=`, and nothing anywhere reports an error — a silent truncation that no amount of typing will catch. Behavioural contracts still live in the docstring and, if they matter, in a shared conformance test suite the provider can run against its own class. **The check is external and late-binding.** A Protocol influences nothing at runtime. If nobody runs a static type checker in CI, the protocol is documentation. Worse, mismatches are reported where the *consumer* is checked, not where the provider is written, so a provider can drift out of conformance and only learn about it from someone else's failing build. Two mitigations: providers who care can inherit the Protocol explicitly, which moves the error to their own class definition; or the provider's own test module can assert conformance with a trivial typed binding, which a checker verifies while the runtime cost is nil. **Signature compatibility is stricter than people expect.** Structural matching considers parameter names and kinds for keyword-callable parameters, so a provider whose method is `def write(self, text: str)` may fail a protocol declaring `def write(self, line: str)` in checkers that compare names. That is worth knowing before you promise a team it will "just work". **When the ABC is still right.** If the scraper must decide at runtime what kind of object it holds — plugin discovery, `isinstance` dispatch, a registry keyed on capability — a nominal base or a registration gives an answer a plain Protocol cannot. If you want to hand implementers real code — retry wrappers, batching, a `write_many` built on `write` — that is a base class, because a Protocol carries no implementation anybody should inherit. And if the number of providers is small and all in the same repository, the coupling an ABC introduces is cheap and the runtime enforcement is free. The mature answer is usually **both, with different jobs**: the consumer declares the narrow Protocol it type-checks against, and the package optionally offers an abstract base class as a convenience for implementers who want the shared behaviour. Providers pick; the scraper's annotation does not care which they picked.
- How does a provider find out it has broken conformance, given the Protocol lives in the consumer?By default it does not — the mismatch is reported wherever the consumer is type-checked. Two fixes: the provider explicitly inherits the Protocol so a checker verifies its class definition, or the provider keeps a one-line typed binding in its own tests that assigns an instance to a variable annotated with the protocol. Both move the failure into the provider's own build.
- A sink conforms structurally but silently truncates every line. What catches that?Nothing in the type system, because structural typing describes shape, not semantics. That gap is closed with documentation of the behavioural contract and a shared conformance test suite that providers run against their implementation — the same discipline you would need with an abstract base class, since overriding an abstract method does not promise correct behaviour either.
- When would you still publish an abstract base class instead?When the consumer needs runtime answers — isinstance dispatch, plugin registries, branching on capability — or when you want implementers to inherit real shared code such as batching or retry logic built on the one abstract method. Both are things a Protocol cannot give, because it contributes nothing to the object at runtime.
An ABC is a club whose members must sign the roster; a Protocol is a job description the hiring manager writes, which anyone already able to do the work satisfies.
saying these in an interview costs you the question
- Thinks a Protocol adds a runtime check to the consumer
- Assumes structural conformance guarantees correct behaviour
- Puts the consumer's Protocol in the provider package
- Declares a wide protocol listing every possible member
- Forgets a Protocol is inert without a type checker in CI
- Says third-party classes can inherit your ABC retroactively