Incorrect type inference with PEP 646 Unpack and self-type annotations
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 20.6k
- Forks
- 3.3k
- PR merge metrics
- PR metrics pending
Description
Bug Report
When a generic class uses TypeVarTuple (PEP 646) and a method has a self-type annotation that "reshapes" the type parameters, mypy incorrectly infers the return type when the class is instantiated with Unpack[Tuple[Any, ...]].
Specifically, mypy infers the return type as None instead of Any | None, leading to false "unreachable code" errors.
To Reproduce
from typing import Generic, TypeVar, Optional, Iterator, Any, Tuple
from typing_extensions import TypeVarTuple, Unpack
_T = TypeVar("_T", bound=Any)
_Ts = TypeVarTuple("_Ts")
class Result(Generic[Unpack[_Ts]]):
"""Generic result class that takes variadic type parameters.
This mimics SQLAlchemy's Result class which is generic over Unpack[_Ts].
"""
def scalar(self: "Result[_T, Unpack[Tuple[Any, ...]]]") -> Optional[_T]:
return None
def test_function() -> Iterator[Any]:
result: Result[Unpack[Tuple[Any, ...]]] = Result()
reveal_type(result)
data = result.scalar()
reveal_type(data)
if data is None:
return
yield from iter(data)
Expected Behavior
If we run pyright, the type of "data" is "Any | None" and the yield from is reachable:
$ pyright mypy_bug_repro.py
/home/classic/dev/sqlalchemy/mypy_bug_repro.py
/home/classic/dev/sqlalchemy/mypy_bug_repro.py:22:17 - information: Type of "result" is "Result[*tuple[Any, ...]]"
/home/classic/dev/sqlalchemy/mypy_bug_repro.py:24:17 - information: Type of "data" is "Any | None"
0 errors, 0 warnings, 2 informations
Actual Behavior
Mypy sees "data" as None in all cases
$ mypy --warn-unreachable mypy_bug_repro.py
mypy_bug_repro.py:22: note: Revealed type is "mypy_bug_repro.Result[Unpack[builtins.tuple[Any, ...]]]"
mypy_bug_repro.py:24: note: Revealed type is "None"
mypy_bug_repro.py:27: error: Statement is unreachable [unreachable]
Found 1 error in 1 file (checked 1 source file)
Your Environment
- Mypy version: mypy 1.19.1 (compiled: yes)
- Python version: 3.14
- typing_extensions version: latest (for TypeVarTuple, Unpack)
- Pyright version (for comparison): 1.1.408
notes
So I used Claude to refactor SQLAlchemy's structures into standalone features. Additional commentary from Claude which may or may not be helpful:
The bug appears to be in mypy's type parameter matching logic. When trying to match:
- Instance type:
Result[Unpack[Tuple[Any, ...]]] - Against self-type:
Result[_T, Unpack[Tuple[Any, ...]]]
Mypy should extract _T = Any from the first element of the unpacked tuple, but instead appears to fail at this extraction, possibly binding _T to Never or treating it as non-existent, resulting in Optional[Never] = None.
downstream SQLAlchemy issues is at https://github.com/sqlalchemy/sqlalchemy/discussions/13089 which is local to the new 2.1.0beta1 release that uses pep-646
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 standalone reproducer described in the issue and run it with mypy --warn-unreachable to confirm the revealed type and unreachable-code error. Trace mypy's type parameter matching for Result[Unpack[Tuple[Any, ...]]] against the self-type annotation; done means scalar() is inferred as Any | None and the yield path is no longer reported unreachable.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100