mypy icon indicating copy to clipboard operation
mypy copied to clipboard

No type narrowing for `in` on dict.keys()

Open matthewhughes934 opened this issue 3 years ago • 1 comments

Bug Report

A an if statement like if x in some_dict.keys() doesn't narrow the type of x to the type of the key of some_dict. This is in contrast with if x in some_dict which does correctly narrow.

(I had a search and didn't find a similar existing discussion, apologies if this isn't the case)

To Reproduce

Run mypy over the following:

from collections.abc import KeysView


def get_via_keys(key: str | None, data: dict[str, str]) -> str:
    if key in data.keys():
        # error: Incompatible return value type (got "Optional[str]", expected "str")
        return key
    return "value"


def get_via_keys_explicit_typing(key: str | None, data: dict[str, str]) -> str:
    keys: KeysView[str] = data.keys()
    if key in keys:
        # error: Incompatible return value type (got "Optional[str]", expected "str")
        return key
    return "value"


def get_check_dict_membership(key: str | None, data: dict[str, str]) -> str:
    if key in data:
        # OK
        return key
    return "value"

Expected Behavior

mypy correctly narrows the type in each of the if statements and the script passes.

Actual Behavior

mypy reports failures as:

script.py:7: error: Incompatible return value type (got "Optional[str]", expected "str")
script.py:15: error: Incompatible return value type (got "Optional[str]", expected "str")
Found 2 errors in 1 file (checked 1 source file)

Your Environment

  • Mypy version used: 0.971 and mypy 0.980+dev.e69bd9a7270daac8db409e8d08400d9d32367c32 (compiled: no) (current master)
  • Mypy command-line flags: mypy <name-of-file-above>
  • Mypy configuration options from mypy.ini (and other config files): the above was run from the root of this project (so whatever's configured there)
  • Python version used: Python 3.10.5
  • Operating system and version: Arch Linux (kernel 5.18.15)

The following allows get_via_keys above to pass under mypy

diff --git a/mypy/checker.py b/mypy/checker.py
index e64cea7b4..852fe8fab 100644
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -6285,6 +6285,7 @@ def builtin_item_type(tp: Type) -> Optional[Type]:
             "builtins.dict",
             "builtins.set",
             "builtins.frozenset",
+            "_collections_abc.dict_keys",
         ]:
             if not tp.args:
                 # TODO: fix tuple in lib-stub/builtins.pyi (it should be generic).

Though I'm not sure how appropriate it would be given the note in the docs for that function:

Note: this is only OK for built-in containers, where we know the behavior of __contains__.

Also, dict_keys is undocumented and a quick git grep --word-regexp 'dict_keys' didn't show much usage for it outside of typeshed.

matthewhughes934 avatar Aug 08 '22 18:08 matthewhughes934

Note: this is only OK for built-in containers, where we know the behavior of contains.

We know that dict.keys() is safe! Please, feel free to send a PR.

sobolevn avatar Aug 09 '22 09:08 sobolevn