[regression] Type narrowing for through `not issubclass(…)` broken for union of TypeVar instance / types in 1.17
Nobody has claimed this yet.
- 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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