2

I want to use a collection that has both Enums and Enum members. But I can't find a way to write it that doesn't cause mypy errors when I try to access the Special Attribute __name__. (See a list of when __name__ can be expected types and members.)

For context a brief explanation about what happens is given by the BDFL in mypy issue #3728. Basically when you iterate over a collection that has items of different types they are joined. So in the next example what mypy is saying is that the Enum and the Enum member are joined to object...

Example 1:

from enum import Enum
from typing import Type

class Color(Enum):
    RED = 1

my_collection: tuple[Type[Color], Color] = (Color, Color.RED)

for item in my_collection:
    if item is Color:
        print(item.__name__)

mypy gives the error:

error: "object" has no attribute "__name__"; maybe "__ne__" or "__new__"?

Example 2:

Next I tried to annotate the variable in the for loop to see if the mypy error gained some more color...

from enum import Enum
from typing import Type, Union

class Color(Enum):
    RED = 1

my_collection: tuple[Type[Color], Color] = (Color, Color.RED)

item: Union[Type[Color], Color]
for item in my_collection:
    if item is Color:
        print(item.__name__)

This time mypy gave 2 errors, the first caused by the incompatible types in the for loop (makes sense), and the second expectable error saying the Enum member doesn't have the __name__ special attribute.

error: Incompatible types in assignment (expression has type "object", variable has type "Union[Type[Color], Color]")

error: Item "Color" of "Union[Type[Color], Color]" has no attribute "__name__"

Example 3:

My next step was trying typing.cast before using __name__ which usually is enough for mypy to restrict the type...

from enum import Enum
from typing import Type, cast

class Color(Enum):
    RED = 1

my_collection: tuple[Type[Color], Color] = (Color, Color.RED)

for item in my_collection:
    if item is Color:
        item = cast(Type[Color], item)
        print(item.__name__)

This time the error was:

error: has no attribute "__name__"

The 3 examples work at run-time, but what's the Pythonic way of iterating over a collection having items of different types that doesn't cause mypy errors? (Using Python 3.9 and mypy 0.812 with default configurations.)

bad_coder
  • 11,289
  • 20
  • 44
  • 72

1 Answers1

1

I looked at several issues on the MyPy's GitHub and I learned two things :

  1. <nothing> should not be showed in the output, according to Guido
  2. <nothing> means that MyPy union'ed the types (for item it is Type[Color] and Color) and the result is nothing (i.e. "an empty set"), so that it types to nothing. In other words, MyPy fails to compute a useful type for item after the cast, see :
    reveal_type(my_collection[0])
    reveal_type(my_collection[1])
    for item in my_collection:
        if item is Color:
            reveal_type(item)
            item = cast(Type[Color], item)
            reveal_type(item)
            print(item.__name__)
    
    $ mypy -V; mypy so70636782.py
    mypy 0.931
    so70636782.py:9: note: Revealed type is "Type[so70636782.Color]"
    so70636782.py:10: note: Revealed type is "so70636782.Color"
    so70636782.py:13: note: Revealed type is "builtins.object"
    so70636782.py:15: note: Revealed type is "<nothing>"
    so70636782.py:16: error: <nothing> has no attribute "__name__"
    Found 1 error in 1 file (checked 1 source file)
    
    I think you hit a limitation of the tool. You may post an issue on GitHub for that.

MyPy is not very good with unions in my experience, so that I recommend you just use the fact that your tuples are typed couples :

color_class, color = my_collection  # unpacking
print(color_class.__name__)

This way MyPy can keep track individually of each variable, each having a type.

Lenormju
  • 4,078
  • 2
  • 8
  • 22
  • I had gotten this far (only posted little over 1 day ago), the issue is the type narrowing does work using cast but not any other method. Additionally, I'll have to revise my question because using `Type` is deprecated and I should have used type[] (but that doesn't change the problem). But the last problem still remains because using the special attribute `__name__` will never work even if narrowing the type correctly (it seems mypy doesn't associate MetaEnum with class) - usually there's some workaround but I haven't found a way to cast to class (where the closest `__name__` would be). – bad_coder Jan 10 '22 at 13:07
  • I also went through GitHub and the specific `__name__` bug hasn't been reported, although there are about 30-40 open Enum specific bugs. The same goes for the several type narrowing bugs that just don't work for Enum even though they work for regular classes. I haven't had time to finish trying out the Literal variants but from what I have tried the results will be the same (narrowing does have more working options in that case). – bad_coder Jan 10 '22 at 13:08
  • I don't have much time right now, I'll come back sometime in the next couple of days to give some feedback on the answer because it should be edited into a slightly different shape (otherwise it won't be much use for future readers). I'll accept the answer if you're willing to incorporate a little bit of feedback into it. – bad_coder Jan 10 '22 at 13:27
  • The `` although interesting doesn't apply to the problem at hand since it is possible to narrow both types back to their original. – bad_coder Jan 10 '22 at 13:38
  • The first sentence of your post seemed to sum your problem : "I want to use a collection that has both Enums and Enum members. But I can't find a way to write it that doesn't cause mypy errors when I try to access the Special Attribute __name__". My solution to you is : don't iterate on it, just unpack it, it is easier for MyPy to check that. I'm also a bit surprised that the `cast` **lost** information (introduced ``), but I'm not a fan of using `cast`s anyway. – Lenormju Jan 10 '22 at 15:24
  • This is a lot more complicated than might seem, the cast you used would work if it were `item = cast(Color, item)` instead of `item = cast(Type[Color], item)`. There's a minimum of 2 mypy bugs that need to be solved between the beginning and the end of the question (although that wasn't clear to me when I wrote it). I'm thinking about splitting the question into 2, asking a new one for the `__name__` bug and keeping this one just for the Enum narrowing bug. But I don't have time right now, so I'll have to do it tomorrow or Wednesday. – bad_coder Jan 10 '22 at 15:47
  • An iterable can be used if narrowing is done right, so unpacking isn't the right solution. – bad_coder Jan 10 '22 at 15:49