0

I have a slightly complicated type situation where the minimum reproducible version I could come up with looks like this:

from __future__ import annotations

from typing import TypeVar

T = TypeVar("T")


class MyClass(list[T]):
    def method(self, num: int) -> MyClass[MyRecursiveType]:
        if num == 1:
            return MyClass([1, 2, 3])

        return MyClass([self, 1, 2, 3])


MyRecursiveType = int | MyClass["MyRecursiveType"]

My thinking here is that MyClass.method(...) should return an instance of MyClass, and that it should contain either int or further instances of MyClass (hence the recursive type)

The issue is that if I run mypy, I get the error on L13:

error: List item 0 has incompatible type "MyClass[T]"; expected "Union[int, MyClass[MyRecursiveType]]" [list-item]

I can get mypy to stop complaining by adding my T to the union in MyRecursiveType and marking it explcitly as a TypeAlias, like this:

from __future__ import annotations

from typing import TypeAlias, TypeVar

T = TypeVar("T")


class MyClass(list[T]):
    def method(self, num: int) -> MyClass[MyRecursiveType]:
        if num == 1:
            return MyClass([1, 2, 3])

        return MyClass([self, 1, 2, 3])


MyRecursiveType: TypeAlias = int | T | MyClass["MyRecursiveType"]

But this doesn't feel quite correct, what would be a more correct solution?

EDIT:

Updating to make example more aligned to my real code, since I did not capture the full issue I think:

from __future__ import annotations

from typing import TypeVar

T = TypeVar("T")


class MyClass(list[T]):
    pass


class MySubClassOne(MyClass[T]):
    def one(self, val: int | MyClass):
        if val == 1:
            return MySubClassOne([1, 2, 3])

        return MySubClassOne([self, 1, 2, 3])

    def two(self, val: int | MyClass):
        if val == 1:
            return MySubClassTwo([1, 2, 3])

        return MySubClassTwo([self, 1, 2, 3])


class MySubClassTwo(MyClass[T]):
    def one(self, val: int | MyClass):
        if val == 2:
            return MySubClassOne([1, 2, 3])

        return MySubClassOne([self, 1, 2, 3])

    def two(self, val: int | MyClass):
        if val == 2:
            return MySubClassTwo([1, 2, 3])

        return MySubClassTwo([self, 1, 2, 3])

So in this example the issue is that different subclasses return each other and could contain arbitrarily deeply nested versions of one another, which I'd like to capture.

Fredrik Nilsson
  • 549
  • 4
  • 13
  • There's something wrong with the code or the type annotations here. `MyClass[T]` is a list of `T`s. `method` can either return a `MyClass[int]` or a `MyClass[int | T]`. There is no scenario where it returns a `MyClass[MyClass[something]]`, so what do you need this recursive type for? – Aran-Fey Feb 20 '23 at 16:45
  • Ah sorry, I made a mistake in my attempt to make a minimal example. I've removed the unpacking of self in the second return to be aligned with what I meant – Fredrik Nilsson Feb 20 '23 at 16:58
  • `self` is an instance of `MyClass[T]`, For most possible values of `T`, it is not an instance of `MyRecursiveType`. Consider what would happen if you had `x: MyClass[str] = MyClass(['a', 'b', 'c'])` and you tried to call `x.method()`. (It wouldn't be valid with `x: MyClass[int] = MyClass([1, 2, 3])` either, for that matter - a `MyClass[int]` still isn't an instance of `MyRecursiveType`, so it can't be an element of a `MyClass[MyRecursiveType]`.) – user2357112 Feb 20 '23 at 17:08
  • Ok. You still don't need a recursive type, though. Now the correct return annotation is simply `-> MyClass[int | typing.Self]`. – Aran-Fey Feb 20 '23 at 17:08
  • Ah sorry agian, I don't think I was still able to actually capture the issue I'm having in the example then. I added a more comprehensive example in an EDIT to my original post. The thing is that I essentially have subclasses of MyClass that return each other, or themselves, in an arbitrarily deeply nested way – Fredrik Nilsson Feb 20 '23 at 17:43
  • So for example what I am trying to capture is that `MySubClassTwo.one(...)` could return something like `MySubClassOne[MySubClassTwo[int], MySubClassOne[MySubClassTwo[int]]]` – Fredrik Nilsson Feb 20 '23 at 17:45
  • This still doesn't require a recursive type. Consider for a moment what your functions are doing. They take in an object of type `MyClass[X]` and put it into a (glorified) list. That's one level of nesting. There's nothing recursive about this. The output is simply a `list[MyClass[X]]`. It doesn't matter whether the input `val` is something simple like a `MyClass[str]` or something complex like a `MyClass[MyClass[MyClass[MyClass[str]]]]`. The function takes an object of type `V = TypeVar('V', bound=MyClass)` as input and returns an object of type `MyClass[V]`. – Aran-Fey Feb 20 '23 at 19:46
  • hmm I'm starting to get your point here, thank you. I can't quite seem to figure how to type this out for my above example though, if I only return say `MyClass[V]` then it misses the case with ints being in it, so I would have guessed then to make the return `MyClass[V | int]` - however this leads to the error `List item 0 has incompatible type "MySubClassOne[T]"; expected "Union[V, int]"` from mypy. I assume I have to change the T type var in some way as well then? – Fredrik Nilsson Feb 20 '23 at 20:59
  • Oh, sorry, I got that mixed up. It's actually the same as before, all you need is `-> MySubClassN[int | Self]`. – Aran-Fey Feb 20 '23 at 21:10
  • but if I do `-> MySubClassN[int | Self]` for `MySubClassOne.two(...)`, dont I miss the case where `MySubClassTwo[MySubClassTwo[int]]` then? Since its neither the `Self` or the `int`? – Fredrik Nilsson Feb 20 '23 at 21:54
  • I don't see a problem. In the `if val == 1:` branch, it returns a `MySubClassTwo[int]`. In the other branch, it returns a `MySubClassTwo[int | Self]`. Altogether, that's `MySubClassTwo[int | Self]`. There is no case where this function returns a `MySubClassTwo[MySubClassTwo[int]]`. It can return a `MySubClassTwo[MySubClassOne[int] | int]` if `self` is of type `MySubClassTwo[int]`, but that's already covered by the `Self` annotation. – Aran-Fey Feb 20 '23 at 22:33
  • I'm noticing me trying to make a simple example went pretty bad, so sorry about all the confusion. There is essentially a case that is missing still, let's say there is another condition that branches out to give a return for `MySubClassOne.two(...)` that ends up with `return MySubClassTwo([val, 1, 2, 3]` - in this case the return would be something like `MySubClassTwo[MyClass[?] | int]` but this is where it feels like there is some recursive type needed. Since it will return a subclass of MyClass inside that can also have further versions inside – Fredrik Nilsson Feb 20 '23 at 22:49
  • oh I think I finally got it, the returns can then just be `MySubClassTwo[int | MyClass[V]]` etc? This seems to check out with mypy – Fredrik Nilsson Feb 20 '23 at 22:58
  • Huge thanks for bearing with me! – Fredrik Nilsson Feb 21 '23 at 08:42

0 Answers0