1

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?

9
  • 2
    This doesn't make any sense to me, either. I think those two comments should be swapped. Commented Feb 9, 2023 at 15:28
  • 2
    Agreed. I submitted a pull request to fix it Commented Feb 9, 2023 at 15:51
  • @chepner can you share the pull request link? I would love to read it Commented Feb 9, 2023 at 15:54
  • 1
    Sure. It's pretty boring, though :) github.com/chepner/peps/pull/1. Commented Feb 9, 2023 at 16:02
  • Ok, I found a PR (not sure if chepners) for it and from the comments I now get it, I'll put an answer myself tomorrow. Commented Feb 9, 2023 at 16:05

1 Answer 1

2

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.

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

Comments

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.