Skip to content

Unable to narrow unions including EllipsisType or NoneType using the identity of their singleton #13117

Closed
@tungol

Description

@tungol

Bug Report

Variables whose type is a union that includes types.EllipsisType can't be narrowed using if foo is Ellipsis. That's unexpected to me, because Ellipsis is documented as the only instance of EllipsisType.

Similar but slightly different issues occur with NoneType and NotImplementedType.

To Reproduce

Minimal reproducing case:

from types import EllipsisType


def working(foo: EllipsisType | int) -> int:
    if isinstance(foo, EllipsisType):
        # Revealed type is "builtins.ellipsis"  (seems to understand the singleton relationship here)
        foo = 1
    reveal_type(foo)  # Revealed type is "builtins.int"
    return foo  # no error

def not_working(foo: EllipsisType | int) -> int:
    if foo is Ellipsis:
        reveal_type(foo)  # Revealed type is "Union[builtins.ellipsis, builtins.int]" (why is int still here?)
        foo = 1
    reveal_type(foo)  # Revealed type is "Union[builtins.ellipsis, builtins.int]"
    return foo  # error: Incompatible return value type (got "Union[ellipsis, int]", expected "int")

Mypy appears to believe that there might be values whose type is EllipsisType but which don't have the same identity as Ellipsis. I don't think that's possible.

Similar things happen with None and NoneType:

from types import NoneType


def working(foo: NoneType | int) -> int:
    if isinstance(foo, NoneType):
        reveal_type(foo)  # Revealed type is "types.NoneType"  (interesting difference from Ellipsis)
        foo = 1
    reveal_type(foo)  # Revealed type is "builtins.int"
    return foo  # no error


def not_working(foo: NoneType | int) -> int:
    if foo is None:
        reveal_type(foo)  # Doesn't show any output (?)
        foo = 1
    reveal_type(foo)  # Revealed type is "Union[types.NoneType, builtins.int]"
    return foo  # Incompatible return value type (got "Union[NoneType, int]", expected "int")

NotImplemented and NotImplementedType pass without error, but appear to do so for the wrong reasons:

from types import NotImplementedType


def working(foo: NotImplementedType | int) -> int:
    if isinstance(foo, NotImplementedType):
        reveal_type(foo)  # Revealed type is "builtins._NotImplementedType"
        foo = 1
    reveal_type(foo)  # Revealed type is "Union[builtins._NotImplementedType, builtins.int]"
    return foo  # no error ??


def also_working(foo: NotImplementedType | int) -> int:
    if foo is NotImplemented:
        reveal_type(foo)  # Revealed type is "Union[builtins._NotImplementedType, builtins.int]"
        foo = 1
    reveal_type(foo)  # Revealed type is "Union[builtins._NotImplementedType, builtins.int]"
    return foo  # no error ??

Your Environment

  • Mypy version used: mypy 0.961 (compiled: yes)
  • Mypy command-line flags: none
  • Mypy configuration options from mypy.ini (and other config files): none
  • Python version used: python Python 3.10.4
  • Operating system and version: macOS 10.15.7

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions