mypy icon indicating copy to clipboard operation
mypy copied to clipboard

Property, classmethod and staticmethod aliases not supported

Open serhiy-storchaka opened this issue 6 years ago • 7 comments

MyPy does not support property aliases. Example:

$ cat a.py 
class A:
    @property
    def f(self) -> int:
        return 1
    g = f

x: int = A().f
y: int = A().g
$ mypy a.py 
a.py:8: error: Incompatible types in assignment (expression has type "Callable[[], int]", variable has type "int")

serhiy-storchaka avatar Apr 19 '19 09:04 serhiy-storchaka

Thanks for reporting!

Yeah, unfortunately properties are known to be often problematic. They were implemented via some special-casing before a more general descriptor support was added.

ilevkivskyi avatar Apr 19 '19 09:04 ilevkivskyi

The problem is not only with properties, but with static and class methods too.

$ cat b.py
class A:
    @classmethod
    def f(cls) -> int:
        return 1
    g = f

x: int = A().f()
y: int = A().g()
$ mypy b.py
b.py:8: error: Invalid self argument "A" to attribute function "g" with type "Callable[[Type[A]], int]"
$ cat c.py
class A:
    @staticmethod
    def f() -> int:
        return 1
    g = f

x: int = A().f()
y: int = A().g()
$ mypy c.py
c.py:8: error: Attribute function "g" with type "Callable[[], int]" does not accept self argument

serhiy-storchaka avatar Apr 19 '19 10:04 serhiy-storchaka

Hm, this is more serious than I though then, raising priority to high.

ilevkivskyi avatar Apr 25 '19 17:04 ilevkivskyi

And apparently with callable properties on a dataclass (though the same errors are returned for a class with callable properties, which do not receive a self passed in when they are called):

from typing import Callable
from dataclasses import dataclass

@dataclass
class Demo:
    call_this: Callable[[str], str]

def call(s: str) -> str:
    return s

demo = Demo(call_this=call)

demo.call_this("test")

When run through mypy, this gets:

demo.py:13: error: Invalid self argument "Demo" to attribute function "call_this" with type "Callable[[str], str]"
demo.py:13: error: Too many arguments

ex-nerd avatar Dec 31 '19 08:12 ex-nerd

Currently, this issue forces rather awkward typing for @classmethods, as we can't even omit the typing inside the class definition. AFAIK, the current only workarounds are:

  • Hint the alias as typing.Callable[..., <return_type>] inside the class definition
  • Bind the alias outside of the class definition (no need to provide a type-hint)

I'm not sure what is supposed to be a convincing way to type the alias; some attempts below:

from __future__ import annotations

from typing import Callable
from types import MethodType


class A:
    @classmethod
    def _internal(cls) -> int:
        return 0

    public_1 = _internal                            # (bad; see `value_1`)
    public_2: classmethod[int] = _internal          # Incompatible types in assignment (expression has type "Callable[[Type[A]], int]", variable has type "classmethod[int]")
    public_3: Callable[[], int] = _internal         # Incompatible types in assignment (expression has type "Callable[[Type[A]], int]", variable has type "Callable[[], int]")
    public_4: Callable[[type[A]], int] = _internal  # (bad; see `value_4`)
    public_5: MethodType = _internal                # Incompatible types in assignment (expression has type "Callable[[Type[A]], int]", variable has type "MethodType")
    public_6: Callable[..., int] = _internal        # OK

public_7 = A._internal                              # OK


value_1: int = A.public_1()  # Too few arguments
value_2: int = A.public_2()  # (bad; see `public_2`)
value_3: int = A.public_3()  # (bad; see `public_3`)
value_4: int = A.public_4()  # Too few arguments
value_5: int = A.public_5()  # (bad; see `public_5`)
value_6: int = A.public_6()  # OK
value_7: int = public_7()    # OK

public_4 / value_4 is especially concerning, as it is contradictory; what mypy thinks is the correct type for the public_4 method causes a mypy error when actually called.

bzoracler avatar Feb 21 '22 04:02 bzoracler

Do you really require it to be an alias? Here's another workaround:

class A:
    @classmethod
    def public_8(cls) -> int:
        return cls._internal()

erictraut avatar Feb 21 '22 15:02 erictraut

Are there any updates on this issue?

mvadari avatar Jul 06 '23 17:07 mvadari