Repository: python/mypy

Issue:
Crash related to partial type in subclass
Mypy crashes when type checking this program:

```py
class A:
    x: str

class B(A):
    x = None
    x = ''
```

Here's the traceback:

```
Traceback (most recent call last):
  File "/Users/jukka/src/mypy/scripts/mypy", line 6, in <module>
    main(__file__)
  File "/Users/jukka/src/mypy/mypy/main.py", line 80, in main
    type_check_only(sources, bin_dir, options, flush_errors)
  File "/Users/jukka/src/mypy/mypy/main.py", line 129, in type_check_only
    flush_errors=flush_errors)
  File "/Users/jukka/src/mypy/mypy/build.py", line 173, in build
    result = _build(sources, options, alt_lib_path, bin_dir, saved_cache, flush_errors)
  File "/Users/jukka/src/mypy/mypy/build.py", line 259, in _build
    graph = dispatch(sources, manager)
  File "/Users/jukka/src/mypy/mypy/build.py", line 2094, in dispatch
    process_graph(graph, manager)
  File "/Users/jukka/src/mypy/mypy/build.py", line 2395, in process_graph
    process_stale_scc(graph, scc, manager)
  File "/Users/jukka/src/mypy/mypy/build.py", line 2579, in process_stale_scc
    graph[id].type_check_first_pass()
  File "/Users/jukka/src/mypy/mypy/build.py", line 1951, in type_check_first_pass
    self.type_checker().check_first_pass()
  File "/Users/jukka/src/mypy/mypy/checker.py", line 229, in check_first_pass
    self.accept(d)
  File "/Users/jukka/src/mypy/mypy/checker.py", line 324, in accept
    stmt.accept(self)
  File "/Users/jukka/src/mypy/mypy/nodes.py", line 704, in accept
    return visitor.visit_class_def(self)
  File "/Users/jukka/src/mypy/mypy/checker.py", line 1252, in visit_class_def
    self.accept(defn.defs)
  File "/Users/jukka/src/mypy/mypy/checker.py", line 324, in accept
    stmt.accept(self)
  File "/Users/jukka/src/mypy/mypy/nodes.py", line 766, in accept
    return visitor.visit_block(self)
  File "/Users/jukka/src/mypy/mypy/checker.py", line 1372, in visit_block
    self.accept(s)
  File "/Users/jukka/src/mypy/mypy/checker.py", line 324, in accept
    stmt.accept(self)
  File "/Users/jukka/src/mypy/mypy/nodes.py", line 810, in accept
    return visitor.visit_assignment_stmt(self)
  File "/Users/jukka/src/mypy/mypy/checker.py", line 1379, in visit_assignment_stmt
    self.check_assignment(s.lvalues[-1], s.rvalue, s.type is None, s.new_syntax)
  File "/Users/jukka/src/mypy/mypy/checker.py", line 1412, in check_assignment
    if self.check_compatibility_all_supers(lvalue, lvalue_type, rvalue):
  File "/Users/jukka/src/mypy/mypy/checker.py", line 1513, in check_compatibility_all_supers
    base_node):
  File "/Users/jukka/src/mypy/mypy/checker.py", line 1571, in check_compatibility_super
    'base class "%s" defined the type as' % base.name())
  File "/Users/jukka/src/mypy/mypy/checker.py", line 2961, in check_subtype
    if is_subtype(subtype, supertype):
  File "/Users/jukka/src/mypy/mypy/subtypes.py", line 83, in is_subtype
    ignore_declared_variance=ignore_declared_variance))
  File "/Users/jukka/src/mypy/mypy/types.py", line 1292, in accept
    return visitor.visit_partial_type(self)
  File "/Users/jukka/src/mypy/mypy/subtypes.py", line 349, in visit_partial_type
    raise RuntimeError
RuntimeError:
```

We can fix this issue by checking the compatibility of attributes with base classes at the end of type checking a class.



Discussion:
Currently this crash happens only with `--no-strict-optional`.

Resolving patch:
diff --git a/mypy/checker.py b/mypy/checker.py
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -1786,7 +1786,6 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type
                                                       infer_lvalue_type)
         else:
             lvalue_type, index_lvalue, inferred = self.check_lvalue(lvalue)
-
             # If we're assigning to __getattr__ or similar methods, check that the signature is
             # valid.
             if isinstance(lvalue, NameExpr) and lvalue.node:
@@ -1803,7 +1802,9 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type
                         else:
                             self.check_getattr_method(signature, lvalue, name)
 
-            if isinstance(lvalue, RefExpr):
+            # Defer PartialType's super type checking.
+            if (isinstance(lvalue, RefExpr) and
+                    not (isinstance(lvalue_type, PartialType) and lvalue_type.type is None)):
                 if self.check_compatibility_all_supers(lvalue, lvalue_type, rvalue):
                     # We hit an error on this line; don't check for any others
                     return
@@ -1833,6 +1834,11 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type
                         # Try to infer a partial type. No need to check the return value, as
                         # an error will be reported elsewhere.
                         self.infer_partial_type(lvalue_type.var, lvalue, rvalue_type)
+                    # Handle None PartialType's super type checking here, after it's resolved.
+                    if (isinstance(lvalue, RefExpr) and
+                            self.check_compatibility_all_supers(lvalue, lvalue_type, rvalue)):
+                        # We hit an error on this line; don't check for any others
+                        return
                 elif (is_literal_none(rvalue) and
                         isinstance(lvalue, NameExpr) and
                         isinstance(lvalue.node, Var) and


Test patch:
diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test
--- a/test-data/unit/check-inference.test
+++ b/test-data/unit/check-inference.test
@@ -2689,3 +2689,31 @@ reveal_type(x)  # E: Revealed type is 'builtins.list[Any]'
 reveal_type(y)  # E: Revealed type is 'builtins.dict[Any, Any]'
 
 [builtins fixtures/dict.pyi]
+
+[case testInheritedAttributeNoStrictOptional]
+# flags: --no-strict-optional
+class A:
+    x: str
+
+class B(A):
+    x = None
+    x = ''
+    reveal_type(x)  # E: Revealed type is 'builtins.str'
+
+[case testIncompatibleInheritedAttributeNoStrictOptional]
+# flags: --no-strict-optional
+class A:
+    x: str
+
+class B(A):
+    x = None
+    x = 2  # E: Incompatible types in assignment (expression has type "int", base class "A" defined the type as "str")
+
+[case testInheritedAttributeStrictOptional]
+# flags: --strict-optional
+class A:
+    x: str
+
+class B(A):
+    x = None  # E: Incompatible types in assignment (expression has type "None", base class "A" defined the type as "str")
+    x = ''