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?
Protocol class was added to Python 3.8 as part of PEP 544 as a mechanism for “structural subtyping.” Basically, it is used to define an interface class that acts as a blueprint for designing other classes. Like classes, interface classes define methods however Unlike classes, these methods are abstract methods.
The static method does not take any specific parameter. Class method can access and modify the class state. Static Method cannot access or modify the class state. The class method takes the class as parameter to know about the state of that class.
A class is a template for creating or instantiating objects within a program while a method is a function that exposes the behavior of an object. Thus, this is the main difference between class and method.
To make a method as class method, add @classmethod decorator before the method definition, and add cls as the first parameter to the method. The @classmethod decorator is a built-in function decorator. In Python, we use the @classmethod decorator to declare a method as a class method.
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.
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.
The second item in the list of foos
, Foo2
is a class not an instance such as Foo1()
. This needs to be specified by wrapping around Type
:
def foreach_foo_call_bar(
foos: Iterable[Union[SupportsBar,
Type[SupportsBar]]]):
# ...
You can also do this without the use of a Protocol
:
def foreach_foo_call_bar(
foos: Iterable[Union[Foo1,
Type[Foo2]]]):
# ...
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