mypy icon indicating copy to clipboard operation
mypy copied to clipboard

Generic method returning nested class

Open llchan opened this issue 6 years ago • 5 comments

  • Are you reporting a bug, or opening a feature request? bug

  • Please insert below the code you are checking with mypy.

    from typing import Generic, TypeVar
    
    T = TypeVar('T', bound='Base')
    
    class Base:
        class Inner: ...
    
    class A(Base):
        class Inner: ...
    
    class Foo(Generic[T]):
        def func(self) -> T.Inner: ...
    
    foo = Foo[A]()
    reveal_type(foo.func())
    
  • What is the actual behavior/output?

    main.pyi:15: error: Revealed type is 'main.A*'
    
  • What is the behavior/output you expect?

    main.pyi:15: error: Revealed type is 'main.A.Inner*'
    

    If the analyzer is not sophisticated enough to figure this out yet, it should raise an error/warn and turn into <nothing> rather than becoming the wrong type.

  • What are the versions of mypy and Python you are using? mypy v0.701 + CPython v3.7.1 Do you see the same issue after installing mypy from Git master? yes, same results with 0.710+dev.bb7dbd5afd84656a62311e5f69a1cef6d06466bc

llchan avatar Apr 17 '19 18:04 llchan

The behavior seems to have changed in the last year. From mypy-play, Python 3.8, mypy latest, I get

main.py:12: error: Name 'T.Inner' is not defined
main.py:15: note: Revealed type is 'Any'
Found 1 error in 1 file (checked 1 source file)

mwchase avatar Mar 30 '20 20:03 mwchase

@mwchase I agree. With mypy v0.761 + CPython v3.7.4 I also get what you pasted above.

llchan avatar Apr 01 '20 13:04 llchan

In my case I'd like to parametrize the return type of different kind of states, but I can't figure out what's the correct way of doing so:

from typing import TypedDict, TypeVar

class State:
    
    class Result(TypedDict): ...
    
    def activate(self) -> Result:
        return {}


class StateA(State):
    
    class Result(State.Result):
        a: bool
    
    def activate(self) -> Result:
        return {"a": True}

...


S = TypeVar("S", bound=State)

class StateMachine:
    
    _active: State
    
    def activate(self, s: State) -> State.Result:
        self._active = s
        return s.activate()

    def better_activate(self, s: S) -> S.Result: # fails at runtime (?)
        self._active = s
        return s.activate()


sm = StateMachine()
a = StateA()

ar = sm.activate(a)
reveal_type(ar)       # ok, `State.Result`
reveal_type(ar["a"])  # ok, key error / `Any` type

ar2 = sm.better_activate(a)
reveal_type(ar2)      # should be `StateA.Result`
reveal_type(ar2["a"]) # should be `bool`
main.py:32: error: Name "S.Result" is not defined
main.py:41: note: Revealed type is "TypedDict('__main__.State.Result', {})"
main.py:42: note: Revealed type is "Any"
main.py:42: error: TypedDict "Result" has no key "a"
main.py:45: note: Revealed type is "Any"
main.py:46: note: Revealed type is "Any"

mypy playground

mttbernardini avatar Sep 07 '22 13:09 mttbernardini

In my case I'd like to parametrize the return type of different kind of states, but I can't figure out what's the correct way of doing so:

I don't know if either of these StateMachine implementations are what you were asking for, but they seem like they could be:

from typing import Any, Generic, Protocol, TypedDict, TypeVar

class BaseDict(TypedDict):
    ...

TR = TypeVar("TR", covariant=True, bound=BaseDict)

class State(Protocol[TR]):
    
    def activate(self) -> TR:
        ...


class AResult(TypedDict):
    a: bool


class StateA(State[AResult]):

    def activate(self) -> AResult:
        return {"a": True}

...


S = TypeVar("S", bound=State)

class StateMachine(Generic[TR]):
    
    _active: State[TR]
    
    def activate(self, s: State[TR]) -> TR:
        self._active = s
        return s.activate()

class AnyStateMachine:
    
    _active: State[Any]
    
    def activate(self, s: State[TR]) -> TR:
        self._active = s
        return s.activate()


sm = StateMachine[AResult]()
a = StateA()

ar = sm.activate(a)
reveal_type(ar)       # ok, `TypedDict('__main__.AResult', {'a': builtins.bool})`
reveal_type(ar["a"])  # ok, `bool`

asm = AnyStateMachine()
aar = asm.activate(a)
reveal_type(aar)       # ok, `TypedDict('__main__.AResult', {'a': builtins.bool})`
reveal_type(aar["a"])  # ok, `bool`

mwchase avatar Sep 07 '22 14:09 mwchase

I don't know if either of these StateMachine implementations are what you were asking for, but they seem like they could be:

Thank you for the proposal. I considered using generics in State (which btw should be defined as class State(Generic[TR]), and not using Protocol, because Protocols are meant to be static analysis entities and cannot be instanced at runtime). This would indeed make it easy in the AnyStateMachine.activate signature by simply using a TypeVar.

The only disadvantage of this approach is that the signature of State.activate would be wrong in general, because the base type always returns an empty dict, while with the generic it would be expected to return the specified type (it's not a big issue because in practice State is supposed to be an Abstract Class). Moreover, the signature of the derived classes becomes a bit more verbose.

You can check this example: mpy playground

mttbernardini avatar Sep 08 '22 08:09 mttbernardini