Skip to content

[regression] Type narrowing for through not issubclass(…) broken for union of TypeVar instance / types in 1.17 #19529

@noirbee

Description

@noirbee

Bug Report

In mypy 1.17.0, type variables for arguments which are defined as the union of a generic TypeVar instance or type are not correctly type-narrowed.

To Reproduce

cf https://mypy-play.net/?mypy=latest&python=3.12&gist=630030506910825d5db3d022cdd4198f

from typing import TypeVar

MyType = TypeVar("MyType")


class HelloMixin:
    a: str


def hello(world: MyType | type[MyType]) -> HelloMixin | None:
    world = world if isinstance(world, type) else type(world)

    reveal_type(world)
    if not issubclass(world, HelloMixin):
        return None

    reveal_type(world)
    res = world()
    reveal_type(res)
    reveal_type(res.a)
    return res

Expected Behavior

In mypy 1.16.1, type checking succeeds, and the variables are correctly type-narrowed, i.e. after the if not issubclass(world, HelloMixin):, world is correctly typed as type[subclass_narrowing.HelloMixin]:

% /tmp/core/venv/bin/mypy --version                 
mypy 1.16.1 (compiled: yes)
% /tmp/core/venv/bin/mypy /tmp/subclass_narrowing.py 
/tmp/subclass_narrowing.py: note: In function "hello":
/tmp/subclass_narrowing.py:13:17: note: Revealed type is "builtins.type"
/tmp/subclass_narrowing.py:17:17: note: Revealed type is "type[subclass_narrowing.HelloMixin]"
/tmp/subclass_narrowing.py:19:17: note: Revealed type is "subclass_narrowing.HelloMixin"
/tmp/subclass_narrowing.py:20:17: note: Revealed type is "builtins.str"
Success: no issues found in 1 source file

Actual Behavior

In mypy 1.17.0:

% ./venv/bin/mypy --version                 
mypy 1.17.0 (compiled: yes)
% ./venv/bin/mypy /tmp/subclass_narrowing.py        
/tmp/subclass_narrowing.py: note: In function "hello":
/tmp/subclass_narrowing.py:13:17: note: Revealed type is "MyType`-1 | type[MyType`-1]"
/tmp/subclass_narrowing.py:17:17: note: Revealed type is "MyType`-1 | type[MyType`-1]"
/tmp/subclass_narrowing.py:19:17: note: Revealed type is "Any | MyType`-1"
/tmp/subclass_narrowing.py:20:17: error: Item "object" of "Any | MyType" has no attribute "a"  [union-attr]
        reveal_type(res.a)
                    ^~~~~
/tmp/subclass_narrowing.py:20:17: note: See https://mypy.rtfd.io/en/stable/_refs.html#code-union-attr for more info
/tmp/subclass_narrowing.py:20:17: note: Revealed type is "Any"
/tmp/subclass_narrowing.py:21:12: error: Incompatible return value type (got "Any | MyType", expected "HelloMixin | None")  [return-value]
        return res
               ^~~
Found 2 errors in 1 file (checked 1 source file)

Note the revealed type of the world argument: in 1.16.1 it starts off as just builtins.type, while 1.17 more precisely types it as MyType-1 | type[MyType-1] before the issubclass() check.

A regular world: type[MyType] does not exhibit this behaviour, nor does concrete types (e.g. using Base | type[Base], with HelloMixin as a Base subclass). This only happens when using an union, world: type[MyType] does not exhibit the bug.

I'm not sure where the problem comes from exactly, but the first reveal_type(world) shows us that the line making sure it's always a type already fails to be narrowed to type[MyType-1]. Using a dedicated variable (i.e. world_type = world if isinstance(world, type) else type(world)) still fails as well.

Your Environment

  • Mypy version used: 1.17
  • Mypy command-line flags:
  • Mypy configuration options from mypy.ini (and other config files):
  • Python version used: 3.12.10

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions