python / python/mypy

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

Open
#19,529 2 comments 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug topic-type-narrowing topic-type-variables
Dominant language
Python
Stars
20.6k
Forks
3.3k
PR merge metrics
PR metrics pending

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the provided reproducer and compare its revealed types under mypy 1.16.1 and 1.17.0, then trace the narrowing logic for the isinstance and issubclass checks. Done means the union of a TypeVar instance and its type narrows correctly, the revealed types match the expected behavior, and the example type-checks without errors.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
devtools
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.