Repository: python/mypy

Issue:
can't narrow a union of type[None]
**Bug Report**

There's something about `type[None]` that (understandably) confuses mypy and makes it start producing nonsense results.

While defining a function that takes arguments of `type[None | A | B]`, mypy is able to narrow to `type[None|A]` (or `type[None|B]`) but any attempt to narrow beyond that causes no change in type. This makes it impossible to produce an exhaustive `if/elif/else` that can assert_never in the else case.

**To Reproduce**

https://mypy-play.net/?mypy=latest&python=3.11&gist=7b278f392c623c63fd5b576c483a0e3c

**Expected Behavior**

I should be able to write an `elif` clause that can `assert_type(cls, type[None])`.
**Actual Behavior**

Currently there's no way to narrow a type `Union[type[None], T]` type. (That I found.)

**Your Environment**

https://mypy-play.net


Discussion:
I agree this is a bug in mypy, but there were a few bugs in the sample code. Here's an updated sample that should pass type checking.

```python
import types
import typing_extensions as t

NoneType = type(None)

def type_map(cls: type[None | int | str]) -> None:
    if issubclass(cls, int):
        t.assert_type(cls, "type[int]")
        return
    else:
        t.assert_type(cls, type[None | str])

    if issubclass(cls, str):
        t.assert_type(cls, "type[str]")
        return
    else:
        t.assert_type(cls, type[None])

    t.assert_type(cls, "type[None]")
    if issubclass(cls, str):
        t.assert_never(cls)
    else:
        t.assert_type(cls, type[None])

    if cls is None:
        t.assert_never(cls)
    else:
        t.assert_type(cls, type[None])

    if issubclass(cls, type(None)):
        t.assert_type(cls, type[None])
    else:
        t.assert_never(cls)

    if issubclass(cls, NoneType):
        t.assert_type(cls, type[None])
    else:
        t.assert_never(cls)

    if issubclass(cls, types.NoneType):
        t.assert_type(cls, type[None])
    else:
        t.assert_never(cls)
```

Incidentally, pyright had the same bug as mypy here. It will be fixed in the next release of pyright.

Resolving patch:
diff --git a/mypy/checker.py b/mypy/checker.py
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -7120,9 +7120,10 @@ def conditional_types_with_intersection(
         possible_target_types = []
         for tr in type_ranges:
             item = get_proper_type(tr.item)
-            if not isinstance(item, Instance) or tr.is_upper_bound:
-                return yes_type, no_type
-            possible_target_types.append(item)
+            if isinstance(item, (Instance, NoneType)):
+                possible_target_types.append(item)
+        if not possible_target_types:
+            return yes_type, no_type
 
         out = []
         errors: list[tuple[str, str]] = []
@@ -7130,6 +7131,9 @@ def conditional_types_with_intersection(
             if not isinstance(v, Instance):
                 return yes_type, no_type
             for t in possible_target_types:
+                if isinstance(t, NoneType):
+                    errors.append((f'"{v.type.name}" and "NoneType"', '"NoneType" is final'))
+                    continue
                 intersection = self.intersect_instances((v, t), errors)
                 if intersection is None:
                     continue
@@ -7173,7 +7177,11 @@ def get_isinstance_type(self, expr: Expression) -> list[TypeRange] | None:
             elif isinstance(typ, TypeType):
                 # Type[A] means "any type that is a subtype of A" rather than "precisely type A"
                 # we indicate this by setting is_upper_bound flag
-                types.append(TypeRange(typ.item, is_upper_bound=True))
+                is_upper_bound = True
+                if isinstance(typ.item, NoneType):
+                    # except for Type[None], because "'NoneType' is not an acceptable base type"
+                    is_upper_bound = False
+                types.append(TypeRange(typ.item, is_upper_bound=is_upper_bound))
             elif isinstance(typ, Instance) and typ.type.fullname == "builtins.type":
                 object_type = Instance(typ.type.mro[-1], [])
                 types.append(TypeRange(object_type, is_upper_bound=True))
@@ -7626,7 +7634,7 @@ def convert_to_typetype(type_map: TypeMap) -> TypeMap:
         if isinstance(t, TypeVarType):
             t = t.upper_bound
         # TODO: should we only allow unions of instances as per PEP 484?
-        if not isinstance(get_proper_type(t), (UnionType, Instance)):
+        if not isinstance(get_proper_type(t), (UnionType, Instance, NoneType)):
             # unknown type; error was likely reported earlier
             return {}
         converted_type_map[expr] = TypeType.make_normalized(typ)
diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -385,6 +385,9 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type:
             if node.typeddict_type:
                 # We special-case TypedDict, because they don't define any constructor.
                 result = self.typeddict_callable(node)
+            elif node.fullname == "types.NoneType":
+                # We special case NoneType, because its stub definition is not related to None.
+                result = TypeType(NoneType())
             else:
                 result = type_object_type(node, self.named_type)
             if isinstance(result, CallableType) and isinstance(  # type: ignore[misc]
@@ -511,13 +514,13 @@ def visit_call_expr_inner(self, e: CallExpr, allow_none_return: bool = False) ->
                 if is_expr_literal_type(typ):
                     self.msg.cannot_use_function_with_type(e.callee.name, "Literal", e)
                     continue
-                if (
-                    node
-                    and isinstance(node.node, TypeAlias)
-                    and isinstance(get_proper_type(node.node.target), AnyType)
-                ):
-                    self.msg.cannot_use_function_with_type(e.callee.name, "Any", e)
-                    continue
+                if node and isinstance(node.node, TypeAlias):
+                    target = get_proper_type(node.node.target)
+                    if isinstance(target, AnyType):
+                        self.msg.cannot_use_function_with_type(e.callee.name, "Any", e)
+                        continue
+                    if isinstance(target, NoneType):
+                        continue
                 if (
                     isinstance(typ, IndexExpr)
                     and isinstance(typ.analyzed, (TypeApplication, TypeAliasExpr))
@@ -4733,6 +4736,8 @@ class LongName(Generic[T]): ...
             return type_object_type(tuple_fallback(item).type, self.named_type)
         elif isinstance(item, TypedDictType):
             return self.typeddict_callable_from_context(item)
+        elif isinstance(item, NoneType):
+            return TypeType(item, line=item.line, column=item.column)
         elif isinstance(item, AnyType):
             return AnyType(TypeOfAny.from_another_any, source_any=item)
         else:


Test patch:
diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test
--- a/test-data/unit/check-narrowing.test
+++ b/test-data/unit/check-narrowing.test
@@ -2022,3 +2022,73 @@ def f(x: Union[int, Sequence[int]]) -> None:
     ):
         reveal_type(x)  # N: Revealed type is "Tuple[builtins.int, builtins.int]"
 [builtins fixtures/len.pyi]
+
+[case testNarrowingIsSubclassNoneType1]
+from typing import Type, Union
+
+def f(cls: Type[Union[None, int]]) -> None:
+    if issubclass(cls, int):
+        reveal_type(cls)  # N: Revealed type is "Type[builtins.int]"
+    else:
+        reveal_type(cls)  # N: Revealed type is "Type[None]"
+[builtins fixtures/isinstance.pyi]
+
+[case testNarrowingIsSubclassNoneType2]
+from typing import Type, Union
+
+def f(cls: Type[Union[None, int]]) -> None:
+    if issubclass(cls, type(None)):
+        reveal_type(cls)  # N: Revealed type is "Type[None]"
+    else:
+        reveal_type(cls)  # N: Revealed type is "Type[builtins.int]"
+[builtins fixtures/isinstance.pyi]
+
+[case testNarrowingIsSubclassNoneType3]
+from typing import Type, Union
+
+NoneType_ = type(None)
+
+def f(cls: Type[Union[None, int]]) -> None:
+    if issubclass(cls, NoneType_):
+        reveal_type(cls)  # N: Revealed type is "Type[None]"
+    else:
+        reveal_type(cls)  # N: Revealed type is "Type[builtins.int]"
+[builtins fixtures/isinstance.pyi]
+
+[case testNarrowingIsSubclassNoneType4]
+# flags: --python-version 3.10
+
+from types import NoneType
+from typing import Type, Union
+
+def f(cls: Type[Union[None, int]]) -> None:
+    if issubclass(cls, NoneType):
+        reveal_type(cls)  # N: Revealed type is "Type[None]"
+    else:
+        reveal_type(cls)  # N: Revealed type is "Type[builtins.int]"
+[builtins fixtures/isinstance.pyi]
+
+[case testNarrowingIsInstanceNoIntersectionWithFinalTypeAndNoneType]
+# flags: --warn-unreachable --python-version 3.10
+
+from types import NoneType
+from typing import final
+
+class X: ...
+class Y: ...
+@final
+class Z: ...
+
+x: X
+
+if isinstance(x, (Y, Z)):
+    reveal_type(x)  # N: Revealed type is "__main__.<subclass of "X" and "Y">"
+if isinstance(x, (Y, NoneType)):
+    reveal_type(x)  # N: Revealed type is "__main__.<subclass of "X" and "Y">1"
+if isinstance(x, (Y, Z, NoneType)):
+    reveal_type(x)  # N: Revealed type is "__main__.<subclass of "X" and "Y">2"
+if isinstance(x, (Z, NoneType)):  # E: Subclass of "X" and "Z" cannot exist: "Z" is final \
+                                  # E: Subclass of "X" and "NoneType" cannot exist: "NoneType" is final
+    reveal_type(x)  # E: Statement is unreachable
+
+[builtins fixtures/isinstance.pyi]