Repository: python/mypy

Issue:
Inferencing type reference of abstract class inferred from concrete type values
```python3
import abc

class MyAbstractType(metaclass=abc.ABCMeta):
  @abc.abstractmethod
  def do(self): pass

class MyConcreteA(MyAbstractType):
  def do(self):
    print('A')

class MyConcreteB(MyAbstractType):
  def do(self):
    print('B')

my_types = {
  'A': MyConcreteA,
  'B': MyConcreteB,
}
reveal_type(my_types)  # Revealed type is 'builtins.dict[builtins.str*, def () -> test-abc.MyAbstractType]'

a = my_types['A']()  # Cannot instantiate abstract class 'MyAbstractType' with abstract attribute 'do'
a.do()
b = my_types['B']()  # Cannot instantiate abstract class 'MyAbstractType' with abstract attribute 'do'
b.do()
```
The above example does not pass the type check as-is.
I need to change it to pass the type check as follows:

```python3
import abc
from typing import Mapping, Type

class MyAbstractType(metaclass=abc.ABCMeta):
  @abc.abstractmethod
  def do(self): pass

class MyConcreteA(MyAbstractType):
  def do(self):
    print('A')

class MyConcreteB(MyAbstractType):
  def do(self):
    print('B')

my_types: Mapping[str, Type[MyAbstractType]] = {  # explicit annotation
  'A': MyConcreteA,
  'B': MyConcreteB,
}
reveal_type(my_types)  # Revealed type is 'typing.Mapping[builtins.str, Type[test-abc.MyAbstractType]]'

a = my_types['A']()
a.do()
b = my_types['B']()
b.do()
```

Since mypy is able to infer `MyAbstractType` from the dictionary in the first snippet, I believe that it should be also able to infer `Type[MyAbstractType]` since the dictionary values are not instances but class names.

My setup: Python 3.8.0 + mypy 0.750

Related issues/PRs:
 - #5135 looks like a similar issue as the author did not use explicit annotation for the dictionaries.
 - #2853 allowed using explicit annotation above.


Discussion:
There are two potential ways to improve the situation:
1. Infer `Type[X]` as the value type instead of a callable type.
2. Make it possible to instantiate an abstract type object represented as a callable type.

The first option is harder to implement and is not backward compatible, so my preference would be option 2.

A potential way to implement 2 would be to only flag as errors direct calls to an ABC type object (e.g. `MyAbstractType()`). If the callee is any other kind of expression or a variable, mypy wouldn't complain, since it could actually be a reference to a concrete derived class at runtime. 
More details about ways to implement option 2. There may be a really simple way: set the `from_type_type` flag to `True`,  when computing join and meet of callables (probably unless some/all items represent an abstract class object).

Resolving patch:
diff --git a/mypy/join.py b/mypy/join.py
--- a/mypy/join.py
+++ b/mypy/join.py
@@ -187,6 +187,11 @@ def visit_callable_type(self, t: CallableType) -> ProperType:
             if is_equivalent(t, self.s):
                 return combine_similar_callables(t, self.s)
             result = join_similar_callables(t, self.s)
+            # We set the from_type_type flag to suppress error when a collection of
+            # concrete class objects gets inferred as their common abstract superclass.
+            if not ((t.is_type_obj() and t.type_object().is_abstract) or
+                    (self.s.is_type_obj() and self.s.type_object().is_abstract)):
+                result.from_type_type = True
             if any(isinstance(tp, (NoneType, UninhabitedType))
                    for tp in get_proper_types(result.arg_types)):
                 # We don't want to return unusable Callable, attempt fallback instead.
diff --git a/mypy/meet.py b/mypy/meet.py
--- a/mypy/meet.py
+++ b/mypy/meet.py
@@ -533,6 +533,11 @@ def visit_callable_type(self, t: CallableType) -> ProperType:
             if is_equivalent(t, self.s):
                 return combine_similar_callables(t, self.s)
             result = meet_similar_callables(t, self.s)
+            # We set the from_type_type flag to suppress error when a collection of
+            # concrete class objects gets inferred as their common abstract superclass.
+            if not ((t.is_type_obj() and t.type_object().is_abstract) or
+                    (self.s.is_type_obj() and self.s.type_object().is_abstract)):
+                result.from_type_type = True
             if isinstance(get_proper_type(result.ret_type), UninhabitedType):
                 # Return a plain None or <uninhabited> instead of a weird function.
                 return self.default(self.s)


Test patch:
diff --git a/test-data/unit/check-abstract.test b/test-data/unit/check-abstract.test
--- a/test-data/unit/check-abstract.test
+++ b/test-data/unit/check-abstract.test
@@ -951,3 +951,51 @@ default = Config({'cannot': 'modify'})  # OK
 default[1] = 2  # E: Unsupported target for indexed assignment
 [builtins fixtures/dict.pyi]
 [typing fixtures/typing-full.pyi]
+
+[case testSubclassOfABCFromDictionary]
+from abc import abstractmethod, ABCMeta
+
+class MyAbstractType(metaclass=ABCMeta):
+  @abstractmethod
+  def do(self): pass
+
+class MyConcreteA(MyAbstractType):
+  def do(self):
+    print('A')
+
+class MyConcreteB(MyAbstractType):
+  def do(self):
+    print('B')
+
+class MyAbstractA(MyAbstractType):
+  @abstractmethod
+  def do(self): pass
+
+class MyAbstractB(MyAbstractType):
+  @abstractmethod
+  def do(self): pass
+
+my_concrete_types = {
+  'A': MyConcreteA,
+  'B': MyConcreteB,
+}
+
+my_abstract_types = {
+  'A': MyAbstractA,
+  'B': MyAbstractB, 
+}
+
+reveal_type(my_concrete_types)  # N: Revealed type is 'builtins.dict[builtins.str*, def () -> __main__.MyAbstractType]' 
+reveal_type(my_abstract_types)  # N: Revealed type is 'builtins.dict[builtins.str*, def () -> __main__.MyAbstractType]' 
+
+a = my_concrete_types['A']()
+a.do()
+b = my_concrete_types['B']()
+b.do()
+
+c = my_abstract_types['A']()  # E: Cannot instantiate abstract class 'MyAbstractType' with abstract attribute 'do'
+c.do()
+d = my_abstract_types['B']()  # E: Cannot instantiate abstract class 'MyAbstractType' with abstract attribute 'do'
+d.do()
+
+[builtins fixtures/dict.pyi]