Mypy: Attribute with the same name as a class in type annotation

Created on 30 Jun 2016  Â·  9Comments  Â·  Source: python/mypy

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?

false-positive needs discussion priority-1-normal

Most helpful comment

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.

All 9 comments

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.

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.

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: ...

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

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)

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.

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.

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

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.

Was this page helpful?
0 / 5 - 0 ratings