Repository navigation
Narrowing str | dict[str, str] with isinstance and .get() #13351
Description
Activity
- addedtopic-type-narrowingConditional type narrowing / binderConditional type narrowing / binder
on Aug 7, 2022 I've been looking at this and figured out so far that when mypy accepts the
CallExprnodeUnion[builtins.str, builtins.dict[builtins.str, builtins.str]]is added toself.type_contextinExpressionChecker. This is propagated toapply_generic_argumentsin applytype.py ascontextwhere it is used to deduce the return type of the call to get.The problem I'm having is that I'm not familiar with how
apply_generic_argumentsworks so I'm not sure where in the function it messes up and what possible solutions would actually totally break its intended function.I looked into this a bit too. Here's a more self-contained reproducer:
from __future__ import annotations from typing import TypeVar T = TypeVar("T") class MyDict: def get_item(self) -> str: ... def get(self, x: T) -> str | T: ... def first() -> None: tag_name: str | MyDict if isinstance(tag_name, MyDict): tag_name = tag_name.get("asd") reveal_type(tag_name) def second() -> None: tag_name: str | MyDict if isinstance(tag_name, MyDict): tag_name = tag_name.get_item() reveal_type(tag_name)
Next I tried:
def third() -> None: tag_name: str | MyDict if isinstance(tag_name, MyDict): reveal_type(tag_name) # MyDict reveal_type(tag_name.get("asd")) # str tag_name = tag_name.get("asd") reveal_type(tag_name) # str | MyDict
When I do
reveal_type(tag_name.get("asd")), that happens in a new type context, and works as expected. But when I dotag_name = tag_name.get("asd"), the type oftag_nameis taken from the type context as a suggestion for what the type oftag_name.get("asd")might be. To make that happen, mypy resolvesTto appropriately.I think the bug is that
str | MyDictis still in the type context by the time we get to the type-narrowed part of the code. When the type narrowing happens, the type oftag_nameshould becomeMyDict, and whatever getsstr | MyDictinto the type context is buggy: it's not looking up the type of the newly-narrowed local variable correctly.I think its about the assignment back to the typed expression. This also exhibits the same behavior and follows a similar data path, but doesn't have type narrowing in the same way.
from typing import TypeVar T = TypeVar("T") def func(default: T) -> str | T: ... def example() -> None: tag_name: str | int tag_name = func("a") reveal_type(tag_name)Its the union of a type and a generic that turns out to be the same type assigned back to a variable with the union type.
Or maybe I'm misunderstanding how the context stack is supposed to work.If the type hint on tag_name is omitted:
def example() -> None: tag_name = func("a") reveal_type(tag_name)then when it gets to the function
infer_function_type_arguments_using_contextit returns early without inferring anything and proceeds to get the right answer. So it could be thatstr | intshouldn't be in the context any more, but I'm not sure.Edit: Made the example more minimal
- addedtopic-type-contextType context / bidirectional inferenceType context / bidirectional inference
on Aug 19, 2022 In my example the type of
tag_nameshould beMyDict, notstr | MyDict, because type narrowing has happened. While it would be nice fortag_name = func("a")to narrow tostr, it's not what this issue is about: here the narrowing has happened already, but the type context isn't picking it up.Your example, modified to match what I consider to be this issue's scope, would be:
from typing import TypeVar T = TypeVar("T") def func(default: T) -> str | T: ... def example() -> None: tag_name: str | int if isinstance(tag_name, str): tag_name = func("a") reveal_type(tag_name) # str | int :(
Fixed in #14151
Bug Report
To Reproduce
Expected Behavior
strstrActual Behavior
Union[str, dict[str, str]]strYour Environment
latest mypy