python / mypy

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

Attribute with the same name as a class in type annotation #1775

Closed JukkaL closed 4 years ago

JukkaL commented 8 years ago

This was reported by Agustin Barto:

class A1:
    pass

class B:
    a1 = None  # type: A1  # Works fine

class C:
    A1 = None  # type: A1  # Complains about Invalid type "A1"

A1Alias = A1
class D:
    A1 = None  # type: A1Alias  # Works

I wonder if the body of C should be valid?

gvanrossum commented 8 years ago

There's a big code smell though, using the class name as the name for a variable containing instances of that class. I'm not sure if we should encourage that.

Reminds me a bit of #1776 though.

tharvik commented 8 years ago

Same issue found with a method definition

class A: pass

class B:
    def a(self) -> A: ...
    def A(self) -> A: ...
    def b(self) -> A: ...

gives

/tmp/asd.py: note: In function "A":
/tmp/asd.py:5: error: Invalid type "A"
/tmp/as3.py: note: In function "b":
/tmp/asd.py:6: error: Invalid type "A"

Also, the error is only raised after the line the name is redefined and then can't be used again.

It is clearly a code smell, but it was found in stdlib while typing datetime.datetime.tzinfo which return a tzinfo.

JukkaL commented 8 years ago

I'd introduce a type alias to work around the issue. For example:

class A: pass

_A = A

class B:
    def a(self) -> _A: ...
    def A(self) -> _A: ...
    def b(self) -> _A: ...
tharvik commented 8 years ago

@JukkaL that's working, I was finding a better example, but it seems that it was more related to #1637; thanks

dmoisset commented 8 years ago

I found other cases of this, even when the name is declared as an attribute. For example this:

class Sentence:
    def __init__(self):
        self.subject = "Bob"
        self.verb = "writes"
        self.object = "a letter"

    def __eq__(self, other: object) -> bool:
        return isinstance(other, Sentence) and other.verb == self.verb

This fails with Invalid type "object" on the definition of __eq__, just because there is an "self.object" attribute in the class, even if python's scope rules indicate that the annotation refers to the object builtin, not the object attribute.

(As a side note, this could get even more confusing with PEP526, you'll have a object: str declaration at the class body level)

ahawker commented 7 years ago

Ran into this one tonight in the ulid-py library after chasing my tail a bit.

Example:

    @property
    def bytes(self) -> bytes:
        """
        Computes the bytes value of the underlying :class:`~memoryview`.

        :return: Memory in bytes form
        :rtype: :class:`~bytes`
        """
        return self.memory.tobytes()

Output:

⇒  mypy ulid
ulid/ulid.py:113: error: Invalid type "bytes"

It may be code smell (property matching a builtin type name) but it's following the same pattern as the stdlib uuid.UUID class.

I'll likely just define some type hint aliases to work-around the problem but it would be nice to see addressed if possible, or at the very least, a more "helpful" error message if this case is detectable vs. other "Invalid type" errors.

gvanrossum commented 7 years ago

I think that in this case mypy doesn't treat the scopes the same way as Python does at runtime, and the example

@property  # or without this
def bytes(self) -> bytes:

should be supported. Though note that the following will be wrong at runtime:

class C:
  def bytes(self) -> bytes: ...
  def frombytes(self, x: bytes): ...

Here, C.__annotations__['frombytes'] is a reference to the bytes method, not to the bytes type! So the problem is order-dependent, and defining an alias is probably a good defensive solution.

ahawker commented 7 years ago

@gvanrossum Thanks for the detailed explanation; defining aliases for those name clashes worked out.

JukkaL commented 4 years ago

This works at least a little better now since mypy switched to the new semantic analyzer. Otherwise, the alias trick seems like a reasonable workaround.

tristanlatr commented 1 year ago

This issue should probably be re-open. It seem that pyright and mypy are not in agreement about the resolving order of annotation.

This code works both on mypy and pyright (with the difference that mypy needs the TypeAlias and pyright doesn't):

from __future__ import annotations
from typing import TypeAlias
class C:
  F:TypeAlias = bytes
  def bytes(self) -> F:
    return b''

But the following code causes mypy to complain about F being not a valid type.

from __future__ import annotations
class C:
  F = b'not a valid type'
  def bytes(self) -> F:
    return F()

class F:
  ...

Looks like pyright starts by looking for annotation names in the global scope, then tries the locals (where mypy does the opposite).

JukkaL commented 1 year ago

The behavior of mypy matches what happens at runtime, which is usually what we want:

class C:
    F = b'foo'
    def f(self) -> F: ...
print(C.f.__annotations__)  # {'return': b'foo'}
tristanlatr commented 1 year ago

@JukkaL note the future import. In this case the runtime behavior is not helping.

tristanlatr commented 1 year ago

I've open this issue on pyright's repo, and it looks like their conclusion is that mypy is inconsistent in this particular case.

A clarification regarding pep 563 and it's implication on scoping rules of annotations would be good.