Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

class object typed with Type[SomeAbstractClass] should not complain about instantiation #1843

Closed
gvanrossum opened this issue Jul 11, 2016 · 12 comments · Fixed by #2853
Closed

Comments

@gvanrossum
Copy link
Member

gvanrossum commented Jul 11, 2016

I wish this code would work:

from typing import Type

from abc import ABC, abstractmethod

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

def f(a: Type[A]) -> A:
    return a()  # E: Cannot instantiate abstract class 'A' with abstract attribute 'm'

The obvious intention is to pass f() a concrete subclass of A.

(UPDATE: My real-world use case is a bit more involved, in asyncio there are some classes that implement an elaborate ABC, and I'd like to just say SelectorEventLoop = ... # type: Type[AbstractEventLoop] rather than having to write out the class definition with all 38 concrete method definitions.)

@elazarg
Copy link
Contributor

elazarg commented Nov 6, 2016

Wouldn't it be better to have Concrete[Type[A]] or something similar, so we can be somewhat safer?

@gvanrossum
Copy link
Member Author

gvanrossum commented Nov 6, 2016 via email

@gvanrossum
Copy link
Member Author

Let's do this. We have such code in our codebase that would be flagged as an error except for another bug (#2430).

@elazarg
Copy link
Contributor

elazarg commented Nov 11, 2016

What exactly should be done? What differentiates A() from T() from cls() ? It feels like we are making two different types, whether or not we choose to give them separate names. We want Type[T] to coerce/downcast into ConcreteType[T] which has the additional capability of being callable, but what are the rules for this coercion?

@gvanrossum
Copy link
Member Author

Consider:

from typing import Type
from abc import abstractmethod

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

def f(cls: Type[A]) -> A:
    return cls()

def g() -> A:
    return A()

Currently both f() and g() have the same error:

error: Cannot instantiate abstract class 'A' with abstract attribute 'm'

I would want f() to be error-free but g() should still have the error.

@elazarg
Copy link
Contributor

elazarg commented Nov 15, 2016

I understand the examples, but not the general rule. What should happen in the following?

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

A1 = A
A1()  # error?

@gvanrossum
Copy link
Member Author

gvanrossum commented Nov 15, 2016 via email

gvanrossum pushed a commit that referenced this issue Mar 27, 2017
Fixes #1843  (It was also necessary to fix few minor things to make this work correctly)

The rules are simple, assuming we have:
```python
class A:
    @AbstractMethod
    def m(self) -> None: pass
class C(A):
    def m(self) -> None:
        ...
```
then
```python
def fun(cls: Type[A]):
    cls() # OK
fun(A) # Error
fun(C) # OK
```
The same applies to variables:
```python
var: Type[A]
var() # OK
var = A # Error
var = C # OK
```
Also there is an option for people who want to pass abstract classes around: type aliases, they work as before. For non-abstract ``A``, ``Type[A]`` also works as before.

My intuition why you opened #1843 is when someone writes annotation ``Type[A]`` with an abstract ``A``, then most probably one wants a class object that _implements_ a certain protocol, not just inherits from ``A``.

NOTE: As discussed in python/peps#224 this behaviour is good for both protocols and usual ABCs.
@vtgn
Copy link

vtgn commented Nov 4, 2024

I'm totally lost with all the comments about this subject. So how typing correctly an abstract class?
How typing this method which take an abstract class as argument and return a concrete subclass of this abstract class?:

def get_concrete[T: ABC](interface: Type[T]) -> Type[T]: ...

Currently, mypy forbids to pass an abstract class as argument of this method. This is very annoying, and one of the worst specifications ever made. :/

@erictraut
Copy link

@vtgn, the typing spec doesn't specifically state whether an abstract class is assignable to type[T]. Mypy disallows this, but pyright does not. I think that both decisions are defensible. It may be a good idea to pick one behavior and codify it in the typing spec for consistency.

Here's a variant of your code that works with mypy:

def get_concrete[T: type[ABC]](interface: T) -> T: ...

@vtgn
Copy link

vtgn commented Nov 4, 2024

@vtgn, the typing spec doesn't specifically state whether an abstract class is assignable to type[T]. Mypy disallows this, but pyright does not. I think that both decisions are defensible. It may be a good idea to pick one behavior and codify it in the typing spec for consistency.

Here's a variant of your code that works with mypy:

def get_concrete[T: type[ABC]](interface: T) -> T: ...

This is the only case I haven't tested in this signature. Thanks for this tips!
But it is still annoying to not be able to type an abstract class directly without this workaround. I've read the specification of python typing about Protocol here https://github.com/python/typing/blob/7117775f5465c6705bb753bd639e6255af380386/docs/spec/protocol.rst#type-and-class-objects-vs-protocols, and this is clearly not a good decision because it doesn't allow to type a protocol class (which is abstract).

And the worst in there, is that the linters type automatically a variable storing an abstract class AC inside a method by type[AC], which is inconsistent with this decision.

class AbstractClass(ABC):

    @staticmethod
    @abstractmethod
    def func(): ...

def meth():
    untypedAC = AbstractClass # OK, but untypedAC is considered as type[AbstractClass] by the linters

    typedAC: type[AbstractClass] = AbstractClass # NOK, the linters refuses AbstractClass to be set into this variable, which is inconsistent with the type given to untypedAC

@vtgn
Copy link

vtgn commented Nov 4, 2024

Finally, there is still a typing issue, but inside the implementation of the method.
Here is an example:

def get_concrete[T: Type[ABC]](module_name: str, class_name: str, interface: T) -> Optional[T]:

    module: Optional[ModuleType] = None
    the_class: Optional[object] = None
    result: Optional[T] = None

    try:
        module = import_module(module_name)
    except ValueError:
        pass
    else:
        if hasattr(module, class_name):
            the_class = getattr(module, class_name)

            if inspect.isclass(the_class):
                if not inspect.isabstract(the_class):
                    if issubclass(the_class, interface):
                        result = the_class # <-- Error of typing for linters

    return result

Incompatible types in assignment (expression has type "type[Any]", variable has type "T | None") mypy [assignment]

Type "type[Any]" is not assignable to declared type "T@get_concrete | None"
Type "type[Any]" is not assignable to type "T@get_concrete | None"
Type "type[Any]" is not assignable to type "T@get_concrete"
"type[Any]" is not assignable to "None" Pylance [reportAssignmentType]

Is casting the only one solution? :/

result = cast(T, the_class)

However, even if I fix this problem by using the cast, there is still a new typing problem when we want to store the result of the method:

untyped_result = get_concrete("module_name", "ClassName", "Interface") # OK, considered as Type[Interface] by the linters

typed_result: Type[Interface] = get_concrete("module_name", "ClassName", "Interface") # mypy error: Can only assign concrete classes to a variable of type "type[BaseModelFactoryInterface]" mypy(type-abstract)

I don't understand how this error is possible here, because the method's signature indicates it returns a Type[Interface] value, which is considered as a concrete class by mypy.

@vtgn
Copy link

vtgn commented Nov 4, 2024

Another typing issue about the same subject:

class A(ABC):
    @staticmethod
    @abstractmethod
    def static_method() -> int:
        pass

class B(A):
    @staticmethod
    def static_method() -> int:
        return 1

class C(B):
    @staticmethod
    def static_method() -> int:
        return 2

def meth[T: Type[ABC]](interface: T, classes: list[T]) -> T:
    if inspect.isabstract(interface):
        c = random.choice(classes)
        return c
    raise ValueError()


result = meth(A, [B, C]) # mypy Error : Cannot infer type argument 1 of "meth" mypy (misc)

The problem disappears if the class C inherits from A instead of B.

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

Successfully merging a pull request may close this issue.

5 participants