mypy icon indicating copy to clipboard operation
mypy copied to clipboard

abstract class shouldn't be considered Callable

Open beauxq opened this issue 3 years ago • 6 comments

Bug Report

An abstract class fits the typing for Callable.

To Reproduce

import abc
from typing import Callable


class A(abc.ABC):
    @abc.abstractmethod
    def m(self) -> None:
        ...


class C(A):
    def m(self) -> None:
        pass


def f() -> A:
    return C()


def t(x: Callable[[], A]) -> None:
    x()


def main() -> None:
    t(f)
    t(C)
    t(A)  # should give typing error - A is not callable because it can't be instantiated
    A()  # mypy correctly reports that A can't be instantiated


if __name__ == "__main__":
    main()

Expected Behavior

mypy should report a typing error if an abstract class is put where a Callable should go

Actual Behavior

mypy doesn't report any error if an abstract class is put where a Callable should go

Your Environment

  • Mypy version used: 0.910
  • Python version used: 3.8.10

beauxq avatar Mar 16 '22 02:03 beauxq

The built-in callable function returns true on the abstract class. I guess the typing error is not raised due to ABC appears to be callable but it's not callable as it can't be instantiated.

mixed-source avatar Mar 16 '22 09:03 mixed-source

I strongly disagree with this. AnyIO has a number of abstract classes where their __new__() methods return an instance of a subclass.

agronholm avatar Dec 19 '22 07:12 agronholm

I strongly disagree with this. AnyIO has a number of abstract classes where their __new__() methods return an instance of a subclass.

Can you provide an example to show that this is a pattern worth supporting?

If a Python API user calls a class, they expect to get an instance of that specific class, not an instance of a subclass.

beauxq avatar Dec 19 '22 15:12 beauxq

AnyIO is an async "meta-framework" that delegates actual I/O to underlying frameworks like asyncio or Trio. Therefore it has classes like Lock and Semaphore which, when instantiated, look up the current event loop implementation (using sniffio) and then use the appropriate back-end to instantiate the actual implementation (which would be a subclass of this abstract class). Implementing this in __new__() is far better than the alternative of having separate factory methods like create_lock() which AnyIO had in the past.

agronholm avatar Dec 20 '22 08:12 agronholm

Implementing this in __new__() is far better than the alternative of having separate factory methods like create_lock()

I disagree with this. Having a separate factory method like create_lock() would be better. The reason it would be better is that if a Python API user calls a class, they expect to get an instance of that specific class, not an instance of a subclass.

beauxq avatar Dec 20 '22 15:12 beauxq

That ship has sailed, and nobody has complained about this design change. It makes it much easier to shift from asyncio to AnyIO when these classes essentially work the same way in both libraries. I dislike that MyPy enforces certain design choices on its users, without them even being mandated by any PEP whatsoever.

agronholm avatar Dec 20 '22 17:12 agronholm