python / python/mypy

Incorrect type inference with PEP 646 Unpack and self-type annotations

Open
#20,651 11 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug topic-pep-646 topic-type-variables
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

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 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.