Skip to content

Type variable return value from class methods #3438

Closed

Description

@orenbenkiki

mypy gets confused about the type of a type variable return value from a class method:

from typing import TypeVar
from typing import Generic

P=TypeVar('P')

class G(Generic[P]):

    @classmethod
    def c(cls) -> 'P':
        assert False

    def s(self) -> 'P':
        assert False

class C:
    pass

class GC(G[C]):
    pass

c = C()
c = GC().s() # OK
c = GC.c() # error: Incompatible types in assignment (expression has type "P", variable has type "C")

This is causing a huge PITA in my code...

Actually, in my code I would really want to make c a @classproperty, that is, be able to write GC.c instead of GC.c(). I can implement a @classproperty decorator by myself, of course, but that would mean giving up on any hope whatsoever of mypy being able to type-check my code. Of course, in an ideal world, one would just be able to say both @property and @classmethod, but they don't compose... Sigh.

I expected the simple @classmethod to work, though. I assume this is a bug? Is there a workaround?

BTW - I am using mypy 0.501 and Python 3.6.0, if that matters...

Activity

  1. ilevkivskyi commented on May 24, 2017

    @ilevkivskyi
    Member

    It is not so clear this is actually a bug. Normally, type arguments are only applied on instances, not on classes (we have even special errors for things like C[int].x). It is less clear in this case, but why can't you use c = GC().c() (it is allowed to call classmethods on instances)?

    (Also you don't need quotes around P in return annotations of s and c.)

  2. orenbenkiki commented on May 24, 2017

    @orenbenkiki
    Author

    Using GC().c():

    • This requires creating an actual instance of GC, which I then just throw away, which is inefficient.

    • This implies that c belongs to the instance, when it actually does belong to the class as a whole. This would be confusing to the reader and seems un-Pythonic.

    In my specific case, c is actually called special_instance, that is, there is some special instance of the class that I want to obtain. Creating an instance just to destroy it to get another instances is, well, "yuck".

    As for "type arguments are only applied on instances, not on classes", that is a nice principle, but, classes are instances (of meta-classes). If I could write:

    class M(type, Generic[P]):
        @property
        def c(cls) -> P:
            ...
    
    class G(Generic[P], metaclass=M[P]):
        ...
    
    class GC(G[C]):
        ...
    
    GC.c  # Has type `C`

    Then I would be a very happy camper - and I would only be applying type arguments to instances here :-)

    However this doesn't work, as meta-classes are not allowed to be generic.

    BTW, I could work around my problem in another (less convenient) way, if I could write:

    def foo(t: type) -> t:
        ...

    That is, I could live with writing x = special_instance_of(GC) instead of GC.special_instance; but mypy doesn't allow for this, either.

    I'm pretty much out of workaround ideas at this point...

  3. gnprice commented on May 24, 2017

    @gnprice
    Collaborator

    That is, I could live with writing x = special_instance_of(GC) instead of GC.special_instance

    If I understand you right, Type[C] will give you what you want:
    https://www.python.org/dev/peps/pep-0484/#the-type-of-class-objects

    Your example would look like

    def foo(t: Type[C]) -> C:
        ...
    

    where C is a type variable.

  4. gvanrossum commented on May 24, 2017

    @gvanrossum
    Member

    @orenbenkiki You might want to tone down your language a bit.

  5. JukkaL commented on May 24, 2017

    @JukkaL
    Collaborator

    Mypy should perhaps reject using a class type variable in a class method (or a static method). Type variables are basically meant to be used in an instance context. If you'd need to use a type variable in a class method, you'd have to define another type variable. However, this wouldn't help with the original example.

    To support the original example properly, it seems to me that we'd need something like a 'class-level' type variable (vs. the currently supported 'instance-level' type variables). The value of the variable would be fixed for all instances of a particular class. However I'm not sure whether something like this would actually be a reasonable (or useful) thing to have.

    Before we even start seriously thinking about a new type system feature we need better justification and real-world examples, though.

  6. gvanrossum commented on May 24, 2017

    @gvanrossum
    Member

    Yeah, we should start by rejecting such class/static methods, since the type variable is only specified by instantiation. Agree it's too early to consider a new feature (IMO this has come up before but usually due to some confusion on the user's part -- better error messages could help them without new type system features).

  7. orenbenkiki commented on May 25, 2017

    @orenbenkiki
    Author

    @gnprice : Somehow I missed Type[...] - my bad. I'll try it out. Thanks!

  8. orenbenkiki commented on May 25, 2017

    @orenbenkiki
    Author

    @gvanrossum : I can see why associating types with classes, as opposed to instances, would be a significant feature. Allowing meta-classes to be generic might side step this (as classes are instances of their meta-class), but I'm not familiar enough with the code to tell - perhaps both features are basically the same, implementation-wise.

    The justification is that a natural and correct programming idiom is currently being rejected.

    Specifically, the following are currently rejected:

    class G(Generic[P]):
    
        cp: P  # Class property
    
        @staticmethod
        def sm() -> P:  # Static method
            ...
    
        @classmethod
        def cm(cls) -> P:  # Class method
            ...

    Having generic class properties/methods that use the type parameters seems like a natural thing to expect, when coming from languages with templates and/or generic types. And, of course, the code runs just fine.

    That said... whether this idiom is common enough to make this high-priority compared to other features is a separate issue. If you are saying that more justification is needed to push this to the top of the list, that's perfectly reasonable.

    I suggest keeping this issue open for a while and seeing whether people upvote it.

  9. gvanrossum commented on May 26, 2017

    @gvanrossum
    Member

    Seriously? You "thumbs up" your own comment?

  10. orenbenkiki commented on May 26, 2017

    @orenbenkiki
    Author

    AFAIK github doesn't allow reactions to an issue, just to a specific comment. Placing the 1st one makes it easier for people to add their own up-vote if they so choose (a button appears right next to it). At least that's the way I saw this being done in other projects. If a different system is used in mypy, I'll be happy to follow it.

  11. elazarg commented on Aug 21, 2017

    @elazarg
    Contributor

    I think this is duplicate of #1337

  12. ilevkivskyi commented on Aug 22, 2017

    @ilevkivskyi
    Member

    Hm, indeed looks like a duplicate of #1337

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions