As @Eric states, there is a real type violation in this design.  However, the suggested trick is not a good idea: claiming that `A.meth1` would accept any `Base` object is a lie, unless `meth1` would only use `Base` members, in which case it could as well have been defined in the `Base` class itself.


The Liskov Substitution Principle is at play here: the type `Base.Callback` could be defined as `Callable[['Base'], bool]`.  When you would fake that `A.meth1` would conform to that signature, it would mean that you're allowed to call `A.meth1(an_instance_of_B)`, since `an_instance_of_B` is also an instance of `Base`.

What you _really_ seem to mean is to create a collection of `Subclass.Callback` functions.  But since the subclass is not known when you're specifying the Base, youl cannot know its type.

To do this, you would need an extra type parameter that would 'curiously recur' in the Subclass declaration, but I'm not sure if this would work.

```
from typing import Callable, Iterable, Generic, TypeVar

TSub = TypeVar('TSub')


class BaseT(Generic[TSub]):
    callbacks: Iterable[Callable[[TSub], bool]] = tuple()

    def fmap(self: TSub):
        return tuple(cb(self) for cb in self.callbacks)


class A(BaseT['A']):

    def meth1(self: 'A'):
        return True

    def meth2(self: 'A'):
        return False


A.callbacks = (A.meth1, A.meth2)

a = A()
print(a.fmap())  # (True, False)
```

BUT

template.py:10: error: "TSub" has no attribute "callbacks"
template.py:22: error: Access to generic instance variables via class is ambiguous

I'm not sure if Python's Generics are up to speed yet.

On Tue, Mar 2, 2021 at 9:38 PM Eric Traut <eric@traut.com> wrote:
The reason this doesn't work is that the instance methods `A.meth1` and `B.meth2` expect `self` to be an instance of their own class (or a subtype thereof). For example, the `self` in `A` is implicitly typed as `A`. The full type of this method is `Callable[[A], bool]`. You are then trying to assign this to a `Callable[[Base], bool]`. Parameter types within a callable are contravariant, so this is a type violation. This makes logical sense if you consider that the callback could be invoked with an instance of `Base` (but not `A`) as the first argument. This would violate the assumption made by `A.meth1` is expecting the caller to pass an instance of `A`.

If you explicitly annotate the `self` parameter in `A.meth1` and `B.meth2` to accept an instance of `Base`, you can eliminate the type violation.

```python
    def meth1(self: Base) -> bool: ...
```

--
Eric Traut
Contributor to Pyright & Pylance
Microsoft Corp.
_______________________________________________
Typing-sig mailing list -- typing-sig@python.org
To unsubscribe send an email to typing-sig-leave@python.org
https://mail.python.org/mailman3/lists/typing-sig.python.org/
Member address: kristoffel.pirard@gmail.com