Skip to content

Conversation

@leonace924
Copy link

@leonace924 leonace924 commented Jan 7, 2026

Fixes #20442

Problem

When a generic class has an unused type parameter, mypy incorrectly returns Any when resolving overloads involving functions that reference that class.

Reproduction

from typing import Any, Generic, overload
from collections.abc import Callable, Hashable, Iterable
from typing_extensions import TypeVar, Self

IndexT0 = TypeVar("IndexT0")  # Unused type variable
T = TypeVar("T")

class Series:
    pass

class _LocIndexerFrame(Generic[T]):
    @overload
    def __getitem__(self, idx: tuple[Callable[[DataFrame], int], str]) -> int: ...
    @overload
    def __getitem__(self, idx: tuple[Callable[[DataFrame], list[Hashable]], str]) -> Series: ...
    @overload
    def __getitem__(self, idx: tuple[Callable[[DataFrame], Any], Iterable[Hashable]]) -> T: ...
    def __getitem__(self, idx: object) -> object:
        return idx

class DataFrame(Generic[IndexT0]):
    @property
    def loc(self) -> _LocIndexerFrame[Self]:
        return _LocIndexerFrame()

def select2(df: DataFrame) -> list[Hashable]:
    return []

def select3(_: DataFrame) -> int:
    return 1

reveal_type(DataFrame().loc[select2, "x"])  # Was: Any, Expected: Series
reveal_type(DataFrame().loc[select3, "x"])  # Was: Any, Expected: int

Root Cause

When DataFrame has an unused type parameter IndexT0, mypy treats bare DataFrame as DataFrame[Any]. This Any propagates into callable types like Callable[[DataFrame[Any]], list[Hashable]].

The any_causes_overload_ambiguity() function in checkexpr.py was too aggressive - it saw the nested Any in the argument and concluded there was ambiguity, even though the Any was in a position that was identical across all overload formals (the callable's argument type), not in the position that differentiated the overloads (the callable's return type).

Solution

Added _any_in_unrelated_position() function that checks whether Any in an argument is in a position that doesn't affect overload differentiation:

Changes

  • mypy/checkexpr.py: Added _any_in_unrelated_position() helper function and integrated it into any_causes_overload_ambiguity()
  • test-data/unit/check-overloading.test: Added two regression test cases

Test Plan

  • Added testOverloadWithNestedAnyInCallableDoesNotCauseAmbiguity - tests direct callable arguments
  • Added testOverloadWithNestedAnyInTupleCallableDoesNotCauseAmbiguity - tests tuple containing callable arguments (matches the original issue)
  • All existing overload tests pass (271 passed, 1 skipped)
  • Full test suite passes (7769 passed, 36 skipped, 7 xfailed)

Contribution by Gittensor, see my contribution statistics at https://gittensor.io/miners/details?githubId=42954461

@leonace924
Copy link
Author

@A5rocks @hauntsaninja would you review my PR? glad to contribute on this project!

@github-actions
Copy link
Contributor

github-actions bot commented Jan 7, 2026

According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅

Copy link
Collaborator

@A5rocks A5rocks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really get what _any_in_unrelated_position is doing and I'm not convinced I should spend the hour it would take to do so :P

What's the logic for it?

# So it's safe to just append everything to the same list.
for formal in formals:
matching_formals.append(matched_callable.arg_types[formal])
if not all_same_types(matching_formals) and not all_same_types(matching_returns):
Copy link
Collaborator

@A5rocks A5rocks Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack, I did in fact misunderstand.

But I feel like this is probably wrong, rather than having a complicated addition to patch things.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is your suggestion? we should have complicated addition? @A5rocks

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be great if you give me the advice in case my direction is wrong.. 😉
Thank you for your time

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I'm not sure! I'm a bit confused about what this code is supposed to do, because I don't think the formals being the same type is necessary.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#20442

{E9BE3B76-9A6C-4B67-896E-DA9B58C1FE1F}

can you check the issue and this screenshot? I misunderstood the requirement from issue?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Inconsistent behaviour with / without a dummy type variable

2 participants