mypy icon indicating copy to clipboard operation
mypy copied to clipboard

Nondeterministic output when using tuple type vars

Open Apakottur opened this issue 1 month ago • 2 comments

Bug Report Mypy gives an inconsistent result when run on the same code.

To Reproduce Create the following file:

# test.py
from typing import Any, Generic, Tuple, TypeVar, overload


_TP = TypeVar("_TP", bound=Tuple[Any, ...])


class MyClass(Generic[_TP]):
    pass


_Single = TypeVar("_Single")

_Multiple = TypeVar(
    "_Multiple",
    bound=tuple[Any, Any] | tuple[Any, Any, Any] | tuple[Any, Any, Any, Any] | tuple[Any, Any, Any, Any, Any],
)


@overload
async def inner(query: MyClass[tuple[_Single]]) -> list[_Single]: ...


@overload
async def inner(query: MyClass[_Multiple]) -> list[_Multiple]: ...


async def inner(query: MyClass[tuple[_Single]] | MyClass[_Multiple]) -> list[_Single] | list[_Multiple]:
    return []


@overload
async def outer(query: MyClass[tuple[_Single]]) -> _Single: ...


@overload
async def outer(query: MyClass[_Multiple]) -> _Multiple: ...


async def outer(query: MyClass[tuple[_Single]] | MyClass[_Multiple]) -> _Single | _Multiple:
    result = await inner(query)
    if result:
        return result[0]
    raise ValueError

And run mypy with:

mypy --strict --no-incremental test.py

Expected Behavior Mypy should give a consistent output, either approve or deny the code.

Actual Behavior I get two different outputs if I run it 3-4 times:

Success: no issues found in 1 source file
test.py:43: error: Returning Any from function declared to return "_Single | _Multiple"  [no-any-return]
Found 1 error in 1 file (checked 1 source file)

Notes The code might seem strange and is just something I cooked out of our real life use case (DB queries with SQLAlchemy). Perhaps I'm using an incorrect pattern here in the first place?

Your Environment

  • Mypy version used: 1.20.0+dev.ad0f41eea63ef227739b66f08afbb67544597d79 (compiled: no)
  • Mypy command-line flags: --strict --no-incremental
  • Mypy configuration options from mypy.ini (and other config files): None
  • Python version used: 3.13.3

Apakottur avatar Dec 01 '25 16:12 Apakottur

Thanks for the great repro!

Bisects to #16345 , which isn't the most helpful

Slightly trimmed down version of your repro:

from typing import Any, Generic, TypeVar, overload

_Single = TypeVar("_Single")
_Multiple = TypeVar("_Multiple", bound=tuple[Any, Any] | tuple[Any, Any, Any])

_TP = TypeVar("_TP")
class MyClass(Generic[_TP]): ...


@overload
async def inner(query: MyClass[_Single]) -> _Single: ...
@overload
async def inner(query: MyClass[_Multiple]) -> _Multiple: ...
async def inner(*a: Any, **kw: Any) -> Any:
    raise NotImplementedError


async def outer(query: MyClass[_Single] | MyClass[_Multiple]) -> _Single | _Multiple:
    result = await inner(query)
    if result:
        return result
    raise ValueError

hauntsaninja avatar Dec 01 '25 20:12 hauntsaninja

Looks like another case of non-commutative join, looking more...

hauntsaninja avatar Dec 01 '25 20:12 hauntsaninja