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 x
on 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
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
.