Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementing Python Protocols using both regular methods and class methods

Assume I have two classes Foo1 and Foo2 that implement a method bar():

  • In Foo1, bar() is a regular method
  • In Foo2, 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?

like image 353
MatanRubin Avatar asked Jun 07 '20 08:06

MatanRubin


People also ask

What are protocol classes in Python?

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.

What is the difference between @staticmethod and @classmethod in Python?

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.

What is difference between class and method in Python?

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.

How do you use class methods in Python?

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.


Video Answer


3 Answers

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.

like image 164
chepner Avatar answered Oct 16 '22 22:10

chepner


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.

like image 21
Evan Grim Avatar answered Oct 16 '22 21:10

Evan Grim


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]]]):
    # ...
like image 5
Philipp H. Avatar answered Oct 16 '22 22:10

Philipp H.