Python Protocols: cannot understand 'class objects vs protocols"

334 Views Asked by At

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?

1

There are 1 best solutions below

0
danielcaballero88 On BEST ANSWER

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.