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?
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:
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.
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.