Skip to content

Type Narrowing for Dictionary Keys Not Working with Literal Types After in Check #18208

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
littleswampman opened this issue Nov 29, 2024 · 4 comments
Labels
feature topic-type-narrowing Conditional type narrowing / binder

Comments

@littleswampman
Copy link

Bug Report

Mypy fails to narrow the type of a dictionary key from str to a Literal type even after an existence check with in operator. This pattern is commonly used and type-safe, as the runtime check guarantees the key exists and thus must be one of the literal values.

To Reproduce

from typing import Literal

KeyType = Literal["a", "b", "c"]
data: dict[KeyType, int] = {"a": 1, "b": 2, "c": 3}

def process(key: str) -> int:
    if key in data:
        return data[key]  # error: Invalid index type "str" for "dict[Literal['a', 'b', 'c'], int]"
    return 0

Playground link: https://mypy-play.net/?mypy=latest&python=3.11&gist=92110930ab70a8949ae51cbcf8b8cb5f

Expected Behavior

After checking key in data, mypy should recognize that key must be one of the literal values defined in KeyType since it exists as a key in the dictionary. Therefore, the dictionary access data[key] should type-check successfully.

Actual Behavior

error: Invalid index type "str" for "dict[Literal['a', 'b', 'c'], int]"; expected type "Literal['a', 'b', 'c']"  [index]

Your Environment

  • Mypy version used: 1.13.0
  • Python version used: 3.11.10
@littleswampman littleswampman added the bug mypy got something wrong label Nov 29, 2024
@JelleZijlstra JelleZijlstra added topic-type-narrowing Conditional type narrowing / binder feature and removed bug mypy got something wrong labels Nov 29, 2024
@radorodopski
Copy link

radorodopski commented Dec 6, 2024

I am having a similar issue with the below code.

from typing import Optional

def func(X: str, Y: str):
    print(f"{X} : {Y}")

my_dict: dict[str, Optional[str]] = {"this": None, "that": "T"}

idx: str = "that"

if my_dict[idx] is None:
    raise ValueError(f"my_dict[{idx}] is None")

if idx not in my_dict:
    raise ValueError(f"{idx} does not exist in my_dict")

if not isinstance(my_dict[idx], str):
    raise ValueError(f"my_dict[{idx}] is not a string")

func(my_dict[idx], "YYY")

I have a dictionary with a key type "str" and it looks like no check ever can get rid of the warning:

main.py:19: error: Argument 1 to "func" has incompatible type "str | None"; expected "str"  [arg-type]

Playground link: https://mypy-play.net/?mypy=latest&python=3.11&gist=7141a47bd7fc21c0ebe82c639db5188e

Not sure if this is the same issue. If it is not - please let me know and I can open a separate issue.

@littleswampman
Copy link
Author

Thank you for sharing this issue. As a mypy user, I find myself in a similar situation, and while I can't be completely certain, your case seems distinct from the original example. It might be worth reporting separately.

My Considerations:

  1. Differences in Type Checking
    Your issue involves type narrowing for dictionary values from Optional[str] to str, which could be different from the key-related Literal type challenges discussed in the original example.

  2. Potential for Duplication
    With nearly 2,800 open issues in the mypy tracker, it's challenging to ensure this hasn't already been addressed. While filtering by label:topic-type-narrowing can help, there are still around 138 issues, which can be overwhelming. I ended up giving up on this method myself.

@sterliakov
Copy link
Collaborator

Is this the same as #3229?

@littleswampman
Copy link
Author

littleswampman commented Jan 9, 2025

No, not exactly the same, I guess...
#3229 focuses on narrowing a Union type when it’s in a list, while this issue is about narrowing dictionary keys that use a Literal type. They both use “in,” but the mechanics behind them are different.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature topic-type-narrowing Conditional type narrowing / binder
Projects
None yet
Development

No branches or pull requests

4 participants