I think I understand the context now.
You have Protocols with the @runtime_checkable decorator. That opts them to work in isinstance on a best-effort basis. As Spencer mentions:
I’d probably recommend not relying on
isinstance(x, protocol), that just checks each method exists, it’s not particularly robust.
Let’s assume you continue using isinstance for now, rather than upgrading to a more-precise check from a runtime type checker.
Now why would mypy error on isinstance(x, proto) when proto: type[P], P = TypeVar("P", bound=ModelProtocol), and ModelProtocol is marked as @runtime_checkable?
I’m pretty sure the answer is that @runtime_checkable (specifically) isn’t currently observable in Python’s typing system. So mypy sees something like P = TypeVar("P", bound=SomeProtocolThatMayNotBeRuntimeCheckable). In this situation I think the correct thing to do is put a # type: ignore[some-error-code] on the isinstance line, since I can prove to myself that use of isinstance is correct here even though mypy isn’t smart enough to.
TypeFormshould accept protocols as well (which is also what the PEP states technically, right?), while it seems it does not.
isinstance only accepts a type[T], not a TypeForm[T], so altering proto: type[P] to be proto: TypeForm[P] will cause mypy to say you’re giving an incorrectly-shaped value to isinstance. Indeed isinstance will raise an exception when given very many kinds of type forms that aren’t types.