Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mypy treats Enum's __call__ as calling __init__ #16712

Open
perey opened this issue Dec 27, 2023 · 1 comment
Open

mypy treats Enum's __call__ as calling __init__ #16712

perey opened this issue Dec 27, 2023 · 1 comment
Labels
bug mypy got something wrong

Comments

@perey
Copy link

perey commented Dec 27, 2023

Bug Report

The standard library's enum.Enum has a class method __call__ that can take a member of the enumeration, or a member's value, and returns that member in either case.

It is also possible to override the enumeration's __init__. The Python docs show an example that uses this to attach additional data to the members.

If this is done, mypy reports an error when calling the class, expecting the call to follow the signature of __init__.

To Reproduce

Playground

This is a stripped-down example of the one from the Python docs. It runs successfully (the asserts pass).

from enum import Enum

class Planet(Enum):
    MERCURY = (3.303e+23, 2.4397e6)

    def __init__(self, mass: float, radius: float) -> None:
        self.mass = mass
        self.radius = radius

mercury = Planet(Planet.MERCURY)
assert mercury is Planet.MERCURY

mercury = Planet((3.303e+23, 2.4397e6))
assert mercury is Planet.MERCURY

Expected Behavior

One possibility, of course, would be for mypy to produce no errors. But this is a somewhat odd situation, so failing that, there should be a way to signal to mypy that this is what's happening.

I couldn't find any such way, short of # type: ignore. Overriding and annotating __call__ didn't work:

    @classmethod
    def __call__(cls, value_or_member: Planet | tuple[float, float]) -> Planet:
        return type(Enum).__call__(cls, value_or_member)

This definition type-checks fine, but it doesn't affect the previous errors. (It's worth noting that this doesn't seem to actually override anything; the calls bypass it and go for the metaclass.)

It is possibly to go the other way and adapt the code to suit mypy, by making the use of __call__ explicit. This type-checks fine:

mercury = Planet.__call__(Planet.MERCURY)
assert mercury is Planet.MERCURY

mercury = Planet.__call__((3.303e+23, 2.4397e6))
assert mercury is Planet.MERCURY

But that seems backwards to me.

Searching for existing issues or Q&As didn't turn up anything that looked like it matched. There are some issues related to the functional API (e.g. #10469, a dupe of #6037), which also uses __call__, but mypy specifically supports that. There's also #15024, but that appears to be about __call__ on an instance.

Actual Behavior

main.py:10: error: Missing positional argument "radius" in call to "Planet"  [call-arg]
main.py:10: error: Argument 1 to "Planet" has incompatible type "Planet"; expected "float"  [arg-type]
main.py:13: error: Missing positional argument "radius" in call to "Planet"  [call-arg]
main.py:13: error: Argument 1 to "Planet" has incompatible type "tuple[float, float]"; expected "float"  [arg-type]
Found 4 errors in 1 file (checked 1 source file)

Your Environment

  • Mypy version used: 0.982 locally, latest on playground
  • Mypy command-line flags and config options: none
  • Python version used: 3.11.6 locally, 3.12 on playground

Technically it's implemented on the metaclass rather than by @classmethod.

Preferably something obvious, to be PEP 20-compliant. 😉 ("There should be one—and preferably only one—obvious way to do it. Although that way may not be obvious at first unless you're Dutch." Maybe the problem is that I'm not Dutch?)

@perey perey added the bug mypy got something wrong label Dec 27, 2023
@electric-coder
Copy link

electric-coder commented Dec 28, 2023

I couldn't find any such way, short of # type: ignore.

The solution has been to use a generic __init__(self, *args), and the same goes for __new__.

Example in the playground.

from enum import Enum

class Planet(Enum):
    MERCURY = (3.303e+23, 2.4397e6)

    def __init__(self, *args) -> None:
        self.mass = args[0]
        self.radius = args[1]

mercury = Planet(Planet.MERCURY)
assert mercury is Planet.MERCURY

mercury = Planet((3.303e+23, 2.4397e6))
assert mercury is Planet.MERCURY

There are some issues related to (...)

There are a lot of mypy issues related to Enum. I gave up on checking for Enum fixes in new mypy releases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug mypy got something wrong
Projects
None yet
Development

No branches or pull requests

2 participants