pythontypingpep

Python Protocols: cannot understand 'class objects vs protocols"


I'm reading PEP 544, specifically the section for class objects vs protocols, and I cannot understand the last example given there, I'll copy paste it here:

A class object is considered an implementation of a protocol if accessing all members on it results in types compatible with the protocol members. For example:

from typing import Any, Protocol

class ProtoA(Protocol):
    def meth(self, x: int) -> int: ...
class ProtoB(Protocol):
    def meth(self, obj: Any, x: int) -> int: ...

class C:
    def meth(self, x: int) -> int: ...

a: ProtoA = C  # Type check error, signatures don't match!
b: ProtoB = C  # OK

I can get the rest of the PEP, but this example seems counterintuitive to me. The way I would think it is that the class C implements the method meth with the same signature as ProtoA, so why the heck is an error in line a: ProtoA = C?

And why b: ProtoB = C is correct? The signature of C.meth is different than the signature of ProtoB.meth (the latter includes an extra argument obj: Any.

Can someone explain this by expanding the concept so I can understand it?


Solution

  • After discussing a bit in the question comments and checking a pull request addressing the example of the question I can understand now why the example is correct and where my reasoning was off. Here it goes:

    Typical case: checking an instance against a Protocol

    Let's expand the example a bit to consider the more common case of checking if an instance of C is an implementation of ProtoA or ProtoB:

    c: ProtoA = C()  # OK
    c: ProtoB = C()  # Type check error, signature don't match!
    

    So, clearly, and as expected, an instance of C is an implementation of ProtoA because the promise of ProtoA is that any implementation of it will have a method meth that can be called as c.meth(2) (2 can be any other integer in this case), and I can clearly do:

    c.meth(2)  # This is correct according to the signature/type hints.
    

    Given case: checking a class against a Protocol

    So, what happens in the given example? What happens is that C has a method meth but it's not defined as a class method, so, C.meth has a different signature than c.meth, in fact, C.meth has the signature that is promised by ProtoB, and not by ProtoA, because to use C.meth directly from the class, I need to pass an argument to satisfy self, which has implicit type Any:

    # This...
    c = C()
    c.meth(2)
    # ...is equivalent to this.
    c = C()
    C.meth(c, 2)
    
    # But the promise is fulfilled with anything as first argument
    # because there is no explicit type hint for `self` and thus it is
    # implicityly typed as `Any`.
    C.meth('anything is fine here', 2)  # this is a correct call*
    
    # *although it might result in an error depending on the implementation 
    # if the `self` argument had runtime requirements not expressed in the 
    # type hint.
    

    So there it is, it had a simple explanation after all.