27

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?

2 Answers 2

10
+25

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.

Sign up to request clarification or add additional context in comments.

2 Comments

Perhaps getting ahead of myself, but if you like this answer, you don't necessarily have to award it the bounty. I am not sure what the correct way to assert that a particular attribute lookup will succeed, only that Protocol is not the way to do it.
Thank you for your answer. While it looks like this would work, I don't think anyone should be doing this to solve this issue (as you said). I will leave this question unanswered for now in the hopes that a better answer comes along.
8

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.

2 Comments

Thanks for pointing me to that issue, looks like exactly the issue I described. Let's hope this gets fixed soon!
@Matan I was even more surprised that in the context of your example foo2: SupportsBar = Foo2 fails to type check properly!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.