TL;DR
type[P]can’t be used with non-concrete types such asProtocolorABC- there’s a divergent behavior between different static type checkers
- as is, the typing specification favors
mypybehavior in popping an error from this use case - it is a useful feature to have in checking at runtime if an object implements an interface of a type, to enstablish a contract
- to allow type checkers (static and runtime) to converge on an unique understanding of the concept, which is used quite a lot, a solution could be found in using
TypeForm, a customAbstractType, or reviewing the typing specification on this point (which I think will never happen concretely, but I may be wrong)
This is a revival of the topic originally discussed in this thread.
The problem
Consider the following code snippet:
from typing import Protocol, TypeVar
class BaseProtocol(Protocol):
def method(self) -> str: ...
class MyProtocol(BaseProtocol, Protocol):
def other_method(self) -> int: ...
class MyClass(BaseProtocol):
def method(self) -> str:
return "Hello"
class MyOtherClass(BaseProtocol):
def method(self) -> str:
return "World"
def other_method(self) -> int:
return 42
T = TypeVar('T', bound=BaseProtocol)
def filter_objects(objects: list[BaseProtocol], obj_type: type[T]) -> list[T]:
return [obj for obj in objects if isinstance(obj, obj_type)]
objs: list[BaseProtocol] = [MyClass(), MyOtherClass()]
# up until now, no problems
# the next line is the problematic one
filtered = filter_objects(objs, MyProtocol)
The objective is to filter a group objects - all inheriting from the same type (in this case BaseProtocol, but it could also be a concrete type) and return a sub-group of objects that structurally match the given input, non-concrete type (a protocol in this case).
At runtime this works fine.
Type checking behavior
When running a type checker (for the scope of this thread I chose mypy, pyright and ty) there are two behaviors. I’m attaching links to playgrounds for each. Each playground has been set in strict mode (I couldn’t find an option for ty so maybe it is not…).
This has been a long standing issue in mypy that people have resolved in saying “the associate error code of mypy, type-abstract, should be removed entirely”. Overall the GitHub issue presents a various amount of reasons why using type[P] is ergonomic, for example this. Additionally, disabling the type-abstract error is not a solution for library developers, because as stated in this comment:
Disabling the error doesn’t help for library authors, because you have to tell all your downstream users to disable it as well
The thing is that according to the typing specification, mypy is right and the others are wrong. Or rephrasing, according to the specification, it looks to me like mypy adheres more strictly to it.
Possible solutions
PEP 747 introduced TypeForm, but as noted in this other thread from the author of PEP 747 himself:
isinstanceonly accepts atype[T], not aTypeForm[T]
So, possible solutions that I could think of:
- modify the behavior of
isinstanceto acceptTypeFormwhen the inner type is a non concrete type - introduce an
AbstractTypethat can be explicitly used for structural checks againstisinstance - revisit the typing specification to allow
type[T]to accept non-concrete types (this is an extreme solution that I don’t think anyone will want to follo)
This is primarely to allow different type checkers to converge on a more concrete idea on how to treat non-concrete types; I don’t pretend that the existing behavior for pyright and ty be changed, but in the long run it would allow situations where using type[ABC] can be covered more easily.
Any of the solutions would require a PEP I would say; never done it but first I wanted to gather some opinions since the topic has been silent for a bit.
Considerations on runtime type checkers
Some users pointed out that this use case would fall short at runtime because isinstance only checks the existance of attributes within the object that adhere to the protocol. While this is a fair point, at this time runtime type checkers have no other way of doing this. At the end of this comment I replied that both typeguard and beartype use precisely isinstance for protocol runtime type checking - so it’s basically a moot point.
As far as I managed to understand, a true, strict runtime checking of protocols can only be done via a custom metaclass that reimplements __instancecheck__. I think beartype does something similar by implementing a custom protocol class inheriting from _ProtocolMeta, but I might be off.