python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
18.38k stars 2.82k forks source link

Mypy misunderstands class-level namespace (spurious `Variable "module.Class.attribute" is not valid as a type`) #12570

Open gwk opened 2 years ago

gwk commented 2 years ago

Consider the following:

from typing import Any

@dataclass
class C:
  dict:object # This attribute name shadows `dict` type, but it should not shadow the name inside of the `class` lexical scope; that's not how python works.
  d:dict # This attribute is annotated as `dict`. The interpreter does not pick up the previous shadowed attribute, but mypy does.

c = C(dict={'a':1}, d={'b':2})
print(c.__annotations__)
assert c.__annotations__['d'] is dict # This proves that mypy is misunderstanding the name `dict`.

mypy 0.942 output:

$ mypy property_name_shadows_type.py
property_name_shadows_type.py:11: error: Variable "property_name_shadows_type.C.dict" is not valid as a type
property_name_shadows_type.py:11: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
Found 1 error in 1 file (checked 1 source file)

Ill-advised as this naming may seem, mypy is making a semantic mistake here. The dict:object line should not alter the namespace in which d:dict is evaluated (I hope that's the correct terminology!). I ran across this case when a machine-generated dataclass used a builtin type name for an attribute name.

Some notes:

JelleZijlstra commented 2 years ago

I guess we could make this work, but it's probably always going to be fragile and it makes sense for a type checker to be a bit stricter here than the runtime. Pyright also rejects your code.

As a workaround, I'd suggest making your dataclass generator always qualify types with something like _builtins.dict (adding import builtins as _builtins).

gwk commented 2 years ago

Hi @JelleZijlstra, first of all thank you for all your hard work on mypy. I have already worked around this in the code generator by adding a trailing underscore to names that collide with builtin types. However I hope I can convince the developer team that this should be viewed as a correctness problem rather than a weird/fragile/pointless use case.

I am curious why you say this is "always going to be fragile". I am just guessing, but it seems to me like python evaluates attribute declarations inside of a class such that they are not visible to each other, while mypy is evaluating the attributes one at a time, adding them into the scope so that they are visible to subsequent declarations. I would think that mypy would want to closely respect the semantics of the language, and that taste should not have much to do with it.

The error message is misleading about the semantics of the language. The scoping rule inside of the class body is not obvious and requires special explanation to learners. Mypy should be an aid to students of the language, not a source of confusing misinformation. The quality of error messages matters a lot, especially for non-experts trying to get a grip on the overall semantics.

jall commented 2 years ago

I'm also hitting this with a type generator I'm writing.

It dumps all types into a single file, but mypy rejects any class properties referencing a type (class) that has the same name as another property in that class

E.g.

# test.py
from typing import Union

class Account:
    name: str

class Post:
    title: str

class Event:
    Id: str
    Account: Account
    ParentObject: Union[Account, Post] # ❌ Variable "test.Event.Account" is not valid as a type  [valid-type]

Quoting the types PEP 563 style doesn't make a difference

class Event:
    Id: str
    Account: Account
    ParentObject: Union["Account", "Post"] # ❌ Variable "test.Event.Account" is not valid as a type  [valid-type]

Unfortunately as I'm generating types based off another system, I don't get to choose what I name things without causing hassle for the end user. If I could lowercase the property names and uppercase the classes, my problem would be solved!

taoufik07 commented 2 years ago

Any updates on this? This also happens when working with frameworks that intentionally uses same names as some of the builtin types e.g. drf generic views and viewsets.