Reputation: 1085
Assume I have two classes Foo1
and Foo2
that implement a method bar()
:
Foo1
, bar()
is a regular methodFoo2
, bar()
is a @classmethod
class Foo1:
def bar(self) -> None:
print("foo1.bar")
class Foo2:
@classmethod
def bar(cls) -> None:
print("Foo2.bar")
Now assume I have a function that accepts a list of "anything that has a bar()
method" and calls it:
def foreach_foo_call_bar(foos):
for foo in foos:
foo.bar()
Calling this function works fine during runtime:
foreach_foo_call_bar([Foo1(), Foo2])
as both Foo1()
and Foo2
has a bar()
method.
However, how can I properly add type hints to foreach_foo_call_bar()
?
I tried creating a PEP544 Protocol
called SupportsBar
:
class SupportsBar(Protocol):
def bar(self) -> None:
pass
and annotating like so:
def foreach_foo_call_bar(foos: Iterable[SupportsBar]):
...
But mypy says:
List item 1 has incompatible type "Type[Foo2]"; expected "SupportsBar"
Any idea how to make this properly annotated?
Upvotes: 27
Views: 4310
Reputation: 5225
This is an open issue with mypy. The BFDL hisself even acknowledged it as incorrect behavior precisely 2 years before @MatanRubin asked this question. It remains unresolved, but was recently (November 2019) marked as high priority so hopefully the example provided here will no longer generate a false positive soon.
Upvotes: 8
Reputation: 531165
The issue appears to be that Protocol
is specifically checking if an instance method is supported, not just that an attribute of the correct name exists. In the case of Foo2
, that means the metaclass needs an instance method named bar
; the following seems to behave as expected and type-checks.
# Define a metaclass that provides an instance method bar for its instances.
# A metaclass instance method is almost equivalent to a class method.
class Barrable(type):
def bar(cls) -> None:
print(cls.__name__ + ".bar")
class Foo1:
def bar(self) -> None:
print("foo1.bar")
class Foo2(metaclass=Barrable):
pass
class SupportsBar(Protocol):
def bar(self) -> None:
pass
def foreach_foo_call_bar(foos: Iterable[SupportsBar]):
for foo in foos:
foo.bar()
I won't claim that converting a class method to a metaclass instance method is a good workaround (in fact, it does nothing for static methods), but it points to this being a fundamental limitation of Protocol
that it doesn't handle arbitrary attributes.
Upvotes: 10