Bypass mypy's "Module has no attribute" on dynamic attribute setting

Viewed 4777

I have the following code on a.py:

class Tags(enum.Flag):
    NONE = 0

    A = enum.auto()
    B = enum.auto()
    C = enum.auto()

# Allow using tags.A instead of tags.Tags.A
globals().update(Tags.__members__)

But when I use it on other files, mypy (rightfully) fails to identify the attributes:

import tags
print(tags.A)  # Module has no attribute "A"

Is there any possible way to bypass this in Python 3.6?

Known solutions (which are not good enough for my case):

  • Use # type: ignore every time you use tags.A
  • Use tags.Tags.A
  • Use __getitem__ in the module level (Works only from Python 3.7)
1 Answers

Personally, what I would do is modify my imports to do:

from tags import Tags

..which would let me refer to the enum items by doing just Tags.A everywhere with minimal fuss.


But if you really want to continue referring to these items from the module namespace instead, one approach is to define a __getattr__ function within tags.py:

class Tags(enum.Flag):
    NONE = 0

    A = enum.auto()
    B = enum.auto()
    C = enum.auto()

def __getattr__(name: str) -> Tags:
    return Tags[name]

At runtime, Python will try calling this function if you try accessing some attribute that wasn't directly defined in the tags module object. Mypy understands this convention and assumes that if __getattr__ is defined, the module is "incomplete". So, it'll use the return type as the type for any missing attributes you try accessing.

You may still want to do globals().update(Tags.__members__) mostly as a performance optimization though, to skip having to actually call __getattr__ function at runtime.

This strategy only really works if tags.py only contains one enum though -- otherwise, you'd need to make the return type something like Union[Tags, MyOtherEnum] (which is unwieldy) or even just Any (which loses the benefits of using a type checker).

This strategy also means mypy won't be able to do more sophisticated type inference and narrowing by taking into account the actual enum value. This is mostly only relevant if you're using Literal enums though.


If these are concerns, you might have to resort to a more brute force approach, like so:

from typing import TYPE_CHECKING

class Tags(enum.Flag):
    NONE = 0

    A = enum.auto()
    B = enum.auto()
    C = enum.auto()

globals().update(Tags.__members__)

if TYPE_CHECKING:
    NONE = Tags.NONE
    A = Tags.A
    B = Tags.B
    C = Tags.C

The TYPE_CHECKING constant is always false at runtime, but is considered to be always true by type checkers.

But if you're going to go to the expense of directly telling mypy about each of these variants, you might as well just skip trying to automatically update the globals and do this instead:

class Tags(enum.Flag):
    NONE = 0

    A = enum.auto()
    B = enum.auto()
    C = enum.auto()

NONE = Tags.NONE
A = Tags.A
B = Tags.B
C = Tags.C

Obviously, having to repeat yourself twice like this is pretty suboptimal, but I don't think there's an easy way around it. You could perhaps mitigate this by creating a script that auto-generates tags.py for you, but that's also fairly suboptimal, just for different reasons.

Related