microsoft / pylance-release

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

Why are type annotations sometimes ignored in favor of Pylance's inferred types (in particular, Unknown)? #6589

Closed mattdeutsch closed 1 hour ago

mattdeutsch commented 3 hours ago

Here's a minimal recreation:

def fn(naked_dict: dict):
    s: str | None = naked_dict.get("key")
    # mousing over s reveals it has type "Unknown | None"
    # and no syntax highlighting is given for string methods on s.
    s.upper()
    # even after removing None as a possibility, no syntax highlighting is given:
    if s:
        s.upper()

From what I can gather, Pylance is typing naked_dict as dict[Unknown, Unknown], and inferring that s must be of type Unknown or None because of the signature of dict.get. That's clearly WAI.

What's surprising to me is that Pylance ignores my type annotation of str | None. I would expect that in cases where I have more information than the type system, that I am able to tell it what I know about the system (in this case, I know that the key "key" in naked_dict, when present, always points to a string). Why does Pylance fight with me on this and claim that s is of type Unknown | None?

I'm quite confident that this is just me misunderstanding the goals of Pylance as a project, so I'm unwilling to file this as a bug. But I would like to understand why this choice has been made, and appreciate the time of any contributors willing to help me understand. Is it a problem with the LSP?

Thanks!

erictraut commented 2 hours ago

I recommend reading this documentation to better understand the core concepts for static typing.

You should think of a type annotation as a declaration. The statement s: str | None declares that any value assigned to variable s must conform with the type str | None. It is the job of a static type checker to enforce such declarations. The Python typing spec provides assignability rules that dictate which types are assignable to other types. For example, a value of type Literal["hi"] is assignable to a variable that is declared to accept values of type str | None, but a value of type Literal[1] is not assignable.

When a value is assigned to a variable, a static type checker will track its locally-narrowed type. This type may be narrower (more specific) than the variable's declared type.

def func(x: str | None):
    reveal_type(x) # str | None
    x = "hi"
    reveal_type(x) # Literal["hi"]
    x = None
    reveal_type(x) # None
    x = 1 # Type violation