EDIT: I re-read your answer and I understand better your point about @runtime_checkable not being seen by type[P], so yes you’re absolutely correct, but it’s a moot point which is more closely related to the relationship between static/runtime type checkers and isinstance when applied to abstract classes in general.
The scope of the problem is actually bigger than it seems. Sticking only to mypy as I don’t know how pyright behaves at this time, the first implies that when annotating proto: type[P] - when putting the bound on P on an explicit typing.Protocol class - the expectation is for proto to be a concrete type, so a class that is neither a pure typing.Protocol, an abc.ABC direct subclass or anything that marks a method as abstractmethod. The full scope of the problem is discussed at length in this GitHub issue which I linked above that shows that the pattern of using abstract types for runtime checking does work but is not acceptable by mypy for dubious reasons. Or rather, mypy developers are making a lot of honestly debatable assumptions on its use.
Also, the @runtime_checkable is observable, because it’s currently the only way for isinstance and issubclass to effectively perform their duties on Protocol classes.
Consider the following:
from typing_extensions import Protocol
class MyProtocolExample(Protocol):
def method_one(self, arg1: int) -> str:
...
def method_two(self, arg2: str) -> int:
...
class ImplementationExample:
def method_one(self, arg1: int) -> str:
return f"Number is {arg1}"
def method_two(self, arg2: str) -> int:
return len(arg2)
print(isinstance(ImplementationExample(), MyProtocolExample)) # Should print: True
At runtime it will fail with the following traceback:
Traceback (most recent call last):
File "C:\sandbox\python\typeform_example.py", line 19, in <module>
print(isinstance(ImplementationExample(), MyProtocolExample)) # Should print: True
File "C:\sandbox\python\.venv\lib\site-packages\typing_extensions.py", line 730, in __instancecheck__
raise TypeError("Instance and class checks can only be used with"
TypeError: Instance and class checks can only be used with @runtime_checkable protocols
When applying runtime_checkable to MyProtocolExample, it will work.
As Spencer mentions:
I’d probably recommend not relying on
isinstance(x, protocol), that just checks each method exists, it’s not particularly robust.
You’ll be surprised to know that, actually, this is how some runtime type checkers do the check for protocols behind the scenes - or rather, the ones I consider the most stable, mantained, used and that support the typing.Protocol checks. So far I identified only typeguard and beartype.
For the first, for example, it explicitily states here:
typing.Protocol: run-time protocols are checked withisinstance()[ndr: annotated via@runtime_checkable], others are ignored
I also asked in beartype here, and I strongly suspect the answer is the same.
That’s why TypeForm could eventually fill the gap in this situation: as type[P] expects a concrete type class (so nothing marked as abstract), TypeForm[P] could be used for generic abstract classes that are not to be instantiated but used as a reference template to check against.
My concern is that I misunderstood the intent of PEP 747, because the behavior described above would impact how isinstance works, and maybe that’s the job of another PEP to try and address. But as it stands, the current PEP 747 draft was a bit misleading for me, in particular in this point, in reference to the example of trycast(MyProtocol, obj)