microsoft / pylance-release

Documentation and issues for Pylance
Creative Commons Attribution 4.0 International
1.71k stars 765 forks source link

`dict.get` is inferred as `Any` after the second call in a chain #6128

Closed Ravencentric closed 3 months ago

Ravencentric commented 3 months ago

Environment data

Code Snippet

data = {"a": 1}

data.get("b", {}).get("c", {}).get()
data.get("b", dict()).get("c", dict()).get()

Code_Z50c1oVRGr

Expected behavior

Auto-complete, highlight and provide hints for dict.get when used in a chain

Actual behavior

Does not auto-complete, highlight or provide hints for dict.get when used in a chain

Logs

Logs

debonte commented 3 months ago

Pylance is working correctly here. It's understanding of how dict.get behaves is based entirely on the stdlib type stubs in typeshed. The relevant overload of get in this case is:

    def get(self, key: _KT, default: _T, /) -> _VT | _T: ...

Pylance doesn't track the contents of containers and also doesn't have any special knowledge of the behavior of get, therefore it will never make an assumption about whether the default value will be returned based on the key name that you are retrieving. It's clear to you that the default value (an empty dictionary) will be returned, but Pylance doesn't know that.

So, the first get call will return either the value type of the data dictionary (int) or the type of the default argument (dict[Any, Any]), therefore the return type is int | dict[Any, Any].

The second call to get has two issues -- one for each of the possible types in that union:

  1. int doesn't have a get method. If you enable type checking, you'll see that the second get call is flagged with a diagnostic because of this -- Cannot access attribute "get" for class "int". Since int.get() doesn't exist, the type checker treats it (and its return value) as type Any. image

  2. get("c", {}) on a dict[Any, Any] can return either Any (the value type of dict[Any, Any]) or dict[Any, Any] (the default argument).

So the return type of this second get call is Any | Any | dict[Any, Any] which is simplified to just Any. Once the type checker is dealing with an object of type Any it can no longer do anything useful. Any method can be called on Any, but any such method is unknown and therefore also of type Any.

The type of dict() is also dict[Any, Any], so the behavior of the other line in your example is the same.

If you want to get completion suggestions on subsequent calls to get you'll need to convince the type checker that the object you're dealing with is a dict. For example, you could change your code to something like this:

data = {"a": 1}

result = data.get("b", {})
if isinstance(result, dict):
    result = result.get("c", {})
if isinstance(result, dict):
    result.  # you will see "get" as a completion suggestion here
Ravencentric commented 3 months ago

Thank you for the detailed explanation