Consider I define a protocol Frobbable
. And further I have a valid implementation of the protocol, and a broken implementation which is missing the .frob()
method:
from typing import Protocol
from abc import abstractmethod
class Frobbable(Protocol):
@abstractmethod
def frob(self) -> None:
raise NotImplementedError
def main(knob: Frobbable) -> None:
knob.frob()
class Knob:
def frob(self) -> None:
print("knob has been frobbed")
class BrokenKnob:
pass
main(Knob())
main(BrokenKnob())
Checking this program with mypy results in an error, as expected:
testprotocol.py:25:6: error: Argument 1 to "main" has incompatible type "BrokenKnob"; expected "Frobbable" [arg-type]
main(BrokenKnob())
^
Found 1 error in 1 file (checked 1 source file)
Unfortunately, it doesn't offer any information about why BrokenKnob
is incompatible: in this case, it is missing .frob()
. Without such information, correcting such an issue in a nontrivial program (with protocols with many methods, and many implementations) becomes a profoundly tedious chore.
Is there any way to get this information from mypy or any other tool without modifying the program? I know I can explicitly subclass Frobbable
, but this rather defeats the purpose of using a Protocol
.
I think this is due to a limitation in the heuristics mypy uses to list missing protocol members. Normally, mypy is supposed to report any missing protocol members, but to avoid generating too many spammy error messages, it doesn't bother doing so if every one of the protocol members are missing or if the number of missing members exceeds 2.
For example, if we tweak your example so it falls under these constraints...
from typing import Protocol
from abc import abstractmethod
class Frobbable(Protocol):
@abstractmethod
def frob(self) -> None:
raise NotImplementedError
@abstractmethod
def bob(self) -> None:
raise NotImplementedError
def main(knob: Frobbable) -> None:
knob.frob()
class BrokenKnob:
def frob(self) -> None:
raise NotImplementedError
main(BrokenKnob())
...we get the more descriptive error message as expected:
test.py:23: error: Argument 1 to "main" has incompatible type "BrokenKnob"; expected "Frobbable"
test.py:23: note: 'BrokenKnob' is missing following 'Frobbable' protocol member:
test.py:23: note: bob
While these heuristics do seem reasonable, I also think they could probably do with some more refinement to better handle use cases like the one you're running into. For example, if all of the members are missing, I think it'd be reasonable for mypy to report a "this object doesn't implement anything in the protocol" error message instead of the more generic one, and perhaps handle cases where there are too many missing members more gracefully. You could maybe try submitting a PR to improve these heuristics, if you're up for it?
If you don't have time, one workaround you could try doing to get the full list is to:
The resulting error seems to list all the missing attributes without having a fixed limit.
error: Cannot instantiate abstract class 'BrokenKnob' with abstract attribute 'frob'
Then, once you finish making your fixes, you can undo your temporary changes.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With