Pyright: [Question] About referencing a class in a class

Created on 24 Dec 2020  路  3Comments  路  Source: microsoft/pyright

Hello, I am new to Pyright/Pylance, but I really like it and would like to use it more!

I have a question about referencing a class inside a class, following @erictraut indications in various other threads I tried this

NothingT = TypeVar("NothingT", bound="Nothing")


class Nothing(Generic[NothingT]):
    def bind(self, fn: Callable[[Any], Any]) -> NothingT:
        return Nothing() #error here

    def __repr__(self) -> str:
        return "Nothing"

but I get the following error

(class) Nothing
Expression of type "Nothing" cannot be assigned to return type "NothingT"
  Type "Nothing" cannot be assigned to type "NothingT"Pylance

(I have tried other things without success but this is what I think is the closest to what I should do)
I am probably doing something wrong but I can't find what.

Thanks a lot for your help!

as designed

Most helpful comment

@BjMrq
Nothing isn't really generic in NothingT, as erictraut said

If you want to make a forward reference, you can
a) use stringly annotations

class Nothing:
    def bind(self, fn: Callable[[Any], Any]) -> "Nothing":
        return self  # or return Nothing(), doesn't really matter

    def __repr__(self) -> str:
        return "Nothing"

b) use new forward-referencing mechanics

from __future__ import annotations

class Nothing:
    def bind(self, fn: Callable[[Any], Any]) -> Nothing:
        return self

    def __repr__(self) -> str:
        return "Nothing"

c) just use type inference

class Nothing:
    def bind(self, fn: Callable[[Any], Any]):
        return self

    def __repr__(self) -> str:
        return "Nothing"

If you want to make a Maybe type (I assume from the names?), maybe you want Nothing to be generic, but in some other parameter

from __future__ import annotations

A = TypeVar("A")
B = TypeVar("B")

class Nothing(Generic[A]):
    def bind(self, fn: Callable[[A], Maybe[B]]) -> Nothing[B]:
        return Nothing()

    def __repr__(self) -> str:
        return "Nothing"

class Just(Generic[A]):
    def __init__(self, /, value: A):
        self._value = value

    def bind(self, fn: Callable[[A], Maybe[B]]) -> Maybe[B]:
        return fn(self._value)

    def __repr__(self) -> str:
        return f"Just({self._value})"

Maybe = Union[Nothing[A], Just[A]]

All 3 comments

There are a few concepts that are important to understand here.

First, when you use a bound type variable, the type that is eventually "captured" by that type variable must be assignable to the bound type. That means it must be the bound type or a subclass thereof.

Second, a type variable is a stand-in for an actual type that gets assigned to that type variable when a generic class or function is specialized. Once a type is assigned to that type variable through specialization, the assigned type effectively replaces the type variable everywhere it appears.

Putting those two concepts into practice, let's look at an example. Suppose you have a subclass of Nothing called Nada. Now, let's say that you allocate a Nothing object that is specialized using a Nada type argument (i.e. a Nothing[Nada]). This is allowed because NothingT is bound to a Nothing instance, and Nada is a subclass of Nothing. Now, everywhere that NothingT is used in your Nothing class implementation, you need to mentally replace it with a Nada instance. You've defined the bind method to return NothingT, but the method implementation doesn't return a NothingT (a Nada in our example); instead it returns a Nothing. This is a type violation because Nothing is not necessarily the same as the type captured by NothingT.

It is very odd to define a generic class that accepts a type parameter that is bound to itself. It's not expressly forbidden in the Python type system, but if you are doing this, it's probably wrong. Based on the sample you provided above, it's not clear to me why this class needs to be generic at all, but that might be because the sample is a simplified version of your actual code.

@BjMrq
Nothing isn't really generic in NothingT, as erictraut said

If you want to make a forward reference, you can
a) use stringly annotations

class Nothing:
    def bind(self, fn: Callable[[Any], Any]) -> "Nothing":
        return self  # or return Nothing(), doesn't really matter

    def __repr__(self) -> str:
        return "Nothing"

b) use new forward-referencing mechanics

from __future__ import annotations

class Nothing:
    def bind(self, fn: Callable[[Any], Any]) -> Nothing:
        return self

    def __repr__(self) -> str:
        return "Nothing"

c) just use type inference

class Nothing:
    def bind(self, fn: Callable[[Any], Any]):
        return self

    def __repr__(self) -> str:
        return "Nothing"

If you want to make a Maybe type (I assume from the names?), maybe you want Nothing to be generic, but in some other parameter

from __future__ import annotations

A = TypeVar("A")
B = TypeVar("B")

class Nothing(Generic[A]):
    def bind(self, fn: Callable[[A], Maybe[B]]) -> Nothing[B]:
        return Nothing()

    def __repr__(self) -> str:
        return "Nothing"

class Just(Generic[A]):
    def __init__(self, /, value: A):
        self._value = value

    def bind(self, fn: Callable[[A], Maybe[B]]) -> Maybe[B]:
        return fn(self._value)

    def __repr__(self) -> str:
        return f"Just({self._value})"

Maybe = Union[Nothing[A], Just[A]]

Hi @erictraut, thanks for the very quick response and explaining those concepts, really helped me put things together, I understand with what I was trying was wrong!

Hello @decorator-factory you got my end goal right, thanks for the examples, made me understand those concepts even better!

I really appreciate your help.

Have a very good day!

Was this page helpful?
0 / 5 - 0 ratings