Search code examples
pythonvisual-studio-codepython-typingpylance

VSCode: Using nonlocal causes variable type "Never"


I'm seeing a strange behaviour of VSCode/Pylance with some Python code. Consider the following minimal example:

#!/usr/bin/env python3

from typing import Optional

def main():
    x: Optional[list] = None
    def f():
        nonlocal x
        x = [10]
    f()
    assert isinstance(x, list)
    y: int = x[0] + 3
    assert isinstance(y, int)
    print(y)

main()

The call to f() obviously sets the outer x to [10]. Hovering over x in the first assert statement (after the function call) shows (variable) x: None, which is ok. But after the assert statement, hovering over x shows (variable) x: Never. This causes both the syntax highlighting and auto completion to fail for xon all following lines, as VSCode/Pylance obviously sees a variable that has kind of "no type". Typing x. does not even show object's builtin members like x.__class__ or x.__doc__. Assigning x to another typed variable does not help. The first assert causes mypy to accept the code, while PyLint says Value 'x' is unsubscriptable, so there seems to be some disagreement between other tools as well. This is a bit annoying, as the code otherwise works fine.

Questions: Is this a bug in Pylance? Or even expected behaviour? Is there any way to force Pylance to see the correct type of x?

Tool versions:

$ code --version
1.56.2
054a9295330880ed74ceaedda236253b4f39a335
x64
$ pylint --version
pylint 2.8.2
astroid 2.5.6
Python 3.8.5 (default, Jan 27 2021, 15:41:15) 
[GCC 9.3.0]
$ mypy --version
mypy 0.800

VSCode Python: v2021.5.842923320
VSCode Pylance: v2021.5.3

Thank you and best regards,
Philipp

EDIT: Posted the issue on GitHub


Solution

  • According to the discussion on GitHub, I would answer the questions as follows:

    • Is this a bug in Pylance? Not a bug, but the type inference implemented in Pyright (the type checker of Pylance) does not consider local mutations caused by external execution flows (such as the run of a callback function containing nonlocal).

    • Or even expected behaviour? Yes, at least in part, considering the point above.

    • Is there any way to force Pylance to see the correct type of x? Yes, use x = cast(list, x) after assert isinstance(x, list).

    With the cast, the minimal example would look like this:

    #!/usr/bin/env python3
    
    from typing import Optional, cast
    
    def main():
        x: Optional[list] = None
        def f():
            nonlocal x
            x = [10]
        f()
        assert isinstance(x, list)
        x = cast(list, x)
        y: int = x[0] + 3  # Now x is reported as '(variable) x: list'
        assert isinstance(y, int)
        print(y)
    
    main()
    

    To summarize the explanations in the GitHub issue a bit more: The line x: Optional[list] = None makes x of type None, as this is what it actually is, no matter how the type is declared. Now, lacking the analysis of the mutation of x inside function f, the type of x is still None when the line assert isinstance(x, list) is reached. Since the type None is no instance of list, for the type checker, this line will never pass. To indicate this, the type of x is therefore set to Never.