How can classes whose instances implement protocols be described?

If I have a protocol, what is the proper type hint for the class which implements that protocol?

All of the type checkers I tried (mypy, pyright, and ty) agree that this is incorrect:

import typing as t

class MyProtocol(t.Protocol):
    def __init__(self, x: int) -> None: ...
    def foo(self, y: int) -> int: ...

class MyImplementation:
    def __init__(self, x: int) -> None:
        self.x = x

    def foo(self, y: int) -> int:
        return self.x + y

t.assert_type(MyImplementation, type[MyProtocol])

MyProtocol matches instances of MyImplementation. But the type of those instances matches…?

I think I could construct an additional protocol here (a __call__ which matches the __init__ signature given), but that seems a bit unnatural. Does the typing spec require that this does not match? I’m curious how this construction “should be” annotated.

If anyone is curious about the usage under which this arises, it came up while I was doing some work with jsonschema. The protocol is Validator, and type[Validator] is used in validator_for.
type[Validator] is being used to express that this function “determines the right class to use”.

I don’t know the context well but would it work for type[Validator] to be something like Callable[[...], Validator]?

1 Like

That looks like an incorrect usage of assert_type(): it checks that two types are equal, not that one is assignable to another.

In fact mypy accepts this:

import typing as t

class MyProtocol(t.Protocol):
    def __init__(self, x: int) -> None: ...
    def foo(self, y: int) -> int: ...

class MyImplementation:
    def __init__(self, x: int) -> None:
        self.x = x

    def foo(self, y: int) -> int:
        return self.x + y

def f(x: type[MyProtocol]):
    pass

f(MyImplementation)

However, as @oscarbenjamin mentions, type[] is not necessarily the right tool to use; it’s unsafe in some ways. If you want to construct the class, it’s safer to write something like Callable[[], Validator].

6 Likes

Ach! Thank you for the correction!
I almost never use assert_type, hence my mistake in thinking it checked subtypes.

The original usage context mixed me up into thinking this was a problem with type[SomeProtocol], but I think it’s actually a matter of the protocol being declared in some way which doesn’t match the implementations.

I’ve boiled down the original usage to

from jsonschema.protocols import Validator
from jsonschema.validators import Draft202012Validator

x: type[Validator] = Draft202012Validator

and it’s clear to me from the pyright error that something is mismatched in these types.

I’ll need to dig into the specifics of this case to figure out how to resolve. It’s not a general typing issue at all.