Skip to content

Commit 231f7cf

Browse files
authored
Fix type inference for index expression with bounded TypeVar (#11434)
Closes #8231 When type of index expression (e.g. `foo[bar]`) is analyzed and left expression (i.e. `foo`) has generic type (`TypeVar`) with upper bound, for some upper bound types this type inference yields wrong result. For example, if upper bound type is instance of `TypeDict`, mypy considers return type of such index expression as `object`: ``` from typing import TypedDict, TypeVar class Data(TypedDict): x: int T = TypeVar("T", bound=Data) def f(data: T) -> int: # line below leads to mypy error: # 'Unsupported operand types for + ("object" and "int")' return data["x"] + 1 ``` The root cause of this issue was in `visit_index_with_type` method from `checkexpr.py` which does type analysis for index expressions. For `TypeVar` left expression code flow goes via default branch which just returns return type of upper bound's `__getitem__`. For some types this return type inference logic operates on a fallback type. For example, for `TypedDict` fallback type is `typing._TypedDict` with `__getitem__` returning just `object`. To fix the issue we added special case to `visit_index_with_type` for `TypeVar` left expression which recursively calls `visit_index_with_type` with `TypeVar` upper bound parameter. This way we always handle upper bounds requiring special treatment correctly. Corner case -- recursive TypeVar `T` with upper bound having `__getitem__` method with `self` having type `T` and returning `T`. In this case when we call `visit_index_with_type` recursively, `TypeVar` types of upper bound `__getitem__` method are erased and `check_method_call_by_name` returns type of upper bound, not `T`. So we don't do recursive `visit_index_with_type` call when upper bound has `__getitem__` method and handle this case in `else` branch. which handles it as expected -- it return `T` as return type.
1 parent 4669929 commit 231f7cf

File tree

2 files changed

+74
-0
lines changed

2 files changed

+74
-0
lines changed

mypy/checkexpr.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2964,6 +2964,9 @@ def visit_index_with_type(self, left_type: Type, e: IndexExpr,
29642964
elif (isinstance(left_type, CallableType)
29652965
and left_type.is_type_obj() and left_type.type_object().is_enum):
29662966
return self.visit_enum_index_expr(left_type.type_object(), e.index, e)
2967+
elif (isinstance(left_type, TypeVarType)
2968+
and not self.has_member(left_type.upper_bound, "__getitem__")):
2969+
return self.visit_index_with_type(left_type.upper_bound, e, original_type)
29672970
else:
29682971
result, method_type = self.check_method_call_by_name(
29692972
'__getitem__', left_type, [e.index], [ARG_POS], e,

test-data/unit/check-typevar-values.test

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,3 +631,74 @@ def g(s: S) -> Callable[[S], None]: ...
631631
def f(x: S) -> None:
632632
h = g(x)
633633
h(x)
634+
635+
[case testTypeVarWithTypedDictBoundInIndexExpression]
636+
from typing import TypeVar
637+
from typing_extensions import TypedDict
638+
639+
class Data(TypedDict):
640+
x: int
641+
642+
643+
T = TypeVar("T", bound=Data)
644+
645+
646+
def f(data: T) -> None:
647+
reveal_type(data["x"]) # N: Revealed type is "builtins.int"
648+
[builtins fixtures/tuple.pyi]
649+
650+
[case testTypeVarWithUnionTypedDictBoundInIndexExpression]
651+
from typing import TypeVar, Union, Dict
652+
from typing_extensions import TypedDict
653+
654+
class Data(TypedDict):
655+
x: int
656+
657+
658+
T = TypeVar("T", bound=Union[Data, Dict[str, str]])
659+
660+
661+
def f(data: T) -> None:
662+
reveal_type(data["x"]) # N: Revealed type is "Union[builtins.int, builtins.str*]"
663+
664+
[builtins fixtures/tuple.pyi]
665+
[builtins fixtures/dict.pyi]
666+
667+
[case testTypeVarWithTypedDictValueInIndexExpression]
668+
from typing import TypeVar, Union, Dict
669+
from typing_extensions import TypedDict
670+
671+
class Data(TypedDict):
672+
x: int
673+
674+
675+
T = TypeVar("T", Data, Dict[str, str])
676+
677+
678+
def f(data: T) -> None:
679+
_: Union[str, int] = data["x"]
680+
[builtins fixtures/tuple.pyi]
681+
[builtins fixtures/dict.pyi]
682+
683+
[case testSelfTypeVarIndexExpr]
684+
from typing import TypeVar, Union, Type
685+
from typing_extensions import TypedDict
686+
687+
T = TypeVar("T", bound="Indexable")
688+
689+
class Indexable:
690+
def __init__(self, index: str) -> None:
691+
self.index = index
692+
693+
def __getitem__(self: T, index: str) -> T:
694+
return self._new_instance(index)
695+
696+
@classmethod
697+
def _new_instance(cls: Type[T], index: str) -> T:
698+
return cls("foo")
699+
700+
def m(self: T) -> T:
701+
return self["foo"]
702+
703+
[builtins fixtures/tuple.pyi]
704+
[builtins fixtures/classmethod.pyi]

0 commit comments

Comments
 (0)