10

I want to create the equivalent of this in Python:

static class Event {}

static class MyEvent extends Event {}

interface Filter<E extends Event> {
    boolean filter(E event);
}

static class MyFilter implements Filter<MyEvent> {
    @Override public boolean filter(MyEvent event) {
        return true;
    }
}

This is my attempt (mypy-play):

from typing import TypeVar, Protocol

class Event:
    pass

class MyEvent(Event):
    pass

E = TypeVar("E", bound=Event)

class Filter(Protocol[E]):
    def filter(self, event: E) -> bool:
        raise NotImplementedError

class MyFilter(Filter):
    def filter(self, event: MyEvent) -> bool:       # should be ok
        raise NotImplementedError

class BadFilter(Filter):
    def filter(self, event: object) -> bool:        # should fail
        raise NotImplementedError

...that fails with main.py:11: error: Invariant type variable 'E' used in protocol where contravariant one is expected. Unless I'm misunderstanding, Java seems to be fine with an invariant one, and this is my idea as well; I don't want various Filters to be compatible with one another. In any case, slapping contravariant=True onto T doesn't work either. So,

Why the protocol needs a contravariant variable? And, how do I make this Python code type check?

3
  • 1
    Your "doesn't work either" link says "Success: no issues found in 1 source file" when I hit "Run". Commented Apr 27, 2020 at 20:17
  • 1
    Oh, wait, you were expecting BadFilter to fail. The contravariance means BadFilter is fine. A filter that can filter arbitrary objects can filter events. Commented Apr 27, 2020 at 20:20
  • @user2357112supportsMonica MyFilter filters subclasses, and BadFilter filters superclasses. surely one of these should be failing? Commented Apr 27, 2020 at 20:24

1 Answer 1

10

Protocols don't allow that, because it breaks subtype transitivity. See PEP 544.

If you have the following two classes:

class A:
    def method(self, arg: int):
        pass

class B(A):
    def method(self, arg: object):
        pass

then B is a valid subclass of A, because B.method can accept any arguments A.method can. However, if you could introduce the following protocol:

T = typing.TypeVar('T')

class Proto(typing.Protocol[T]):
    def method(self, arg: T):
        pass

then A would satisfy Proto[int], but B would not, due to the invariance of T.

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

5 Comments

i suppose that if following my Java idea, that is if A was explicitly implementing the protocol, subclassing it like that would be an error and this issue wouldn't arise. I guess I want to either use something other than Protocol or use a contravariant TypeVar instead. This is my latest attempt at the latter—looks good?
@squirrel: Seems fine at first glance. foo: Filter = BadFilter2() passes because Foo is interpreted as Foo[Any], and Any is basically the "don't check me" type. It's sort of like using raw types in Java.
aha, thanks. any idea why it's interpreted as Filter[Any] instead of Filter[E] as it was defined? I'm not sure it is like it works in Java as Java needs explicit declaration of interfaces and simply wouldn't allow me making something like BadFilter2
@squirrel Late response, but: type annotations are optional and relatively recent in Python. Any is the default because otherwise typing old code would be prohibitive. Type-checking tools like Mypy generally have options to disable implicit (and even explicit) Any, but they're not enabled by default so that people can adopt these tools gradually.
Note that while the issue is explained well in this answer, you can specify T as being contravariant by adding contravariant=True. This makes sense, and is also recommended in e.g. VS Code.

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.