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

Check enum members are compared by identity #5356

Open
scop opened this issue Nov 21, 2021 · 17 comments
Open

Check enum members are compared by identity #5356

scop opened this issue Nov 21, 2021 · 17 comments
Labels
Checkers Related to a checker Enhancement ✨ Improvement to a component Good first issue Friendly and approachable by new contributors Hacktoberfest Help wanted 🙏 Outside help would be appreciated, good for new contributors Needs PR This issue is accepted, sufficiently specified and now needs an implementation

Comments

@scop
Copy link
Contributor

scop commented Nov 21, 2021

Current problem

Enum members should be compared by identity rather than equality: https://docs.python.org/3/library/enum.html#comparisons

Enumeration members are compared by identity:

Desired solution

Extending singleton-comparison to cover this would seem to be one possible implementation.

Additional context

No response

@scop scop added Enhancement ✨ Improvement to a component Needs triage 📥 Just created, needs acknowledgment, triage, and proper labelling labels Nov 21, 2021
@Pierre-Sassoulas Pierre-Sassoulas added Checkers Related to a checker and removed Needs triage 📥 Just created, needs acknowledgment, triage, and proper labelling labels Nov 21, 2021
@Pierre-Sassoulas Pierre-Sassoulas added this to the 2.13.0 milestone Nov 21, 2021
@Pierre-Sassoulas
Copy link
Member

Thank you, I did not do that myself, but the doc is very clear about it and it definitely make sense to avoid issue with the "real value" being compared to something semantically different.

@Pierre-Sassoulas
Copy link
Member

Some additional consideration. Considering the following code:

            if action in {
                VariableVisitConsumerAction.RETURN,
                VariableVisitConsumerAction.CONSUME,
            }:
                return

Should pylint suggest this ?

            if action is VariableVisitConsumerAction.RETURN or action is VariableVisitConsumerAction.CONSUME:
                return

@scop
Copy link
Contributor Author

scop commented Nov 27, 2021

For completeness, yes I think it would be good. Or

            if any(action is x for x in (VariableVisitConsumerAction.RETURN, VariableVisitConsumerAction.CONSUME)):
                return

...which subjectively scales better stylewise if there are many things to compare against.

But if it makes things harder, basic comparisons would be a good first step, no need to delay this to get everything covered IMHO.

@cdce8p
Copy link
Member

cdce8p commented Nov 27, 2021

I would recommend this:

if action in {
    VariableVisitConsumerAction.RETURN,
    VariableVisitConsumerAction.CONSUME,
}:
    return

It's a single set contains operation that should be faster and easier to read, especially compared to this:

if any(action is x for x in (VariableVisitConsumerAction.RETURN, VariableVisitConsumerAction.CONSUME)):
    return

A function call (any), generator expression, and individual is comparisons.

--

from enum import Enum
from timeit import timeit

class VariableVisitConsumerAction(Enum):
    RETURN = 0
    CONSUME = 1
    NONE = 2

action = VariableVisitConsumerAction.RETURN

ftuple = (
    VariableVisitConsumerAction.RETURN,
    VariableVisitConsumerAction.CONSUME,
    VariableVisitConsumerAction.NONE,
)
fset = frozenset(ftuple)


def func1():
    if action in fset:
        return True
    return False

def func2():
    return any(action is x for x in ftuple)

print(timeit(func1, number=1_000_000))
print(timeit(func2, number=1_000_000))
0.3418969129998004
0.7927539209922543

The result isn't truly representative as it depends on action (e.g. for NONE the difference would be bigger) and if the set / tuple are written inline. It's enough for a general idea though.

@scop
Copy link
Contributor Author

scop commented Nov 27, 2021

That proposal surely is much easier on the eye, and I'm not surprised at all that it's more performant. However it arguably is not correct, == vs is is the point of this issue. For example:

>>> from enum import IntEnum
>>> class Enum1(IntEnum):
...  FOO = 1
... 
>>> class Enum2(IntEnum):
...  BAR = 1
... 
>>> Enum1.FOO in (Enum2.BAR,)
True
>>> any(Enum1.FOO is x for x in (Enum2.BAR,))
False

Maybe someone can come up with a cleaner expression that would use is semantics for these collection cases?

@cdce8p
Copy link
Member

cdce8p commented Nov 27, 2021

That proposal surely is much easier on the eye, and I'm not surprised at all that it's more performant. However it arguably is not correct, == vs is is the point of this issue.

It will only not work for IntEnum, but that is by design.

Comparisons against non-enumeration values will always compare not equal
(again, IntEnum was explicitly designed to behave differently, [...])

https://docs.python.org/3/library/enum.html#comparisons

@Pierre-Sassoulas
Copy link
Member

OK, I read the documentation too fast the first time (only the part Enumeration members are compared by identity and not the following text). And did not test properly. There are no issues when using == vs is.

>>> class Color(Enum):
...     RED = 1
...     BLUE = 2
... 
>>> class Colored(Enum):
...     RED = 1
...     BLUE = 2
... 
>>> Colored.RED in [Color.RED]
False
>>> Colored.RED == Color.RED
False

It seems that the only issue that can happen is with IntEnum which were designed to be able to do that and probably are used for this reason when they are used.

I think adding a check to advise using is for Enum when both use work perfectly would be highly opinionated and not desirable for a default check. Warning for IntEnum on the other hand would be very seldom useful, because who is using an obscure subclass of Enum instead of main Enum class by mistake ? It could be an extension though.

@scop
Copy link
Contributor Author

scop commented Nov 29, 2021

To me the IntEnum thing is, as the quoted snippet reads, "Comparisons against non-enumeration values ...". My example and this issue is not really comparing against non-enumeration values, it's about comparing enum member values (possibly even between different enums which likely is an error) against each other.

It makes perfect sense to compare IntEnum members to ints with == when one wants that kind of a numeric comparison, but when one wants a typesafe comparison of enum values against each other (which is to me the common case), is would be the right thing to do. I don't think this is opinionated, but then again, who knows, I might be opinionated on that one.

Besides IntEnum, there will be StrEnum in 3.11 which IIRC exhibits the same behavior. And there can be user defined enums that have that.

But yeah, I do think the check should apply only when one compares enum values to other enum values. And it does get perhaps somewhat convoluted with collections, at least untyped ones.

@scop scop changed the title Check enums members for identity comparison Check enum members area compared by identity Nov 29, 2021
@scop scop changed the title Check enum members area compared by identity Check enum members are compared by identity Nov 29, 2021
@scop
Copy link
Contributor Author

scop commented Nov 29, 2021

As far as opinionatedness goes, we do have singleton-comparison enabled by default already, and IMO the checks to use is when comparing explicitly against None or the booleans is in the same boat as doing it for enum values. Gut feeling says it's actually even more important for enum members, as with them it's more likely to cause real bugs.

@Pierre-Sassoulas
Copy link
Member

Pierre-Sassoulas commented Nov 29, 2021

So if I understand you well it's comparing enum by value that is a problem, not using is or ==. Do you want something like that ?

from enum import Enum


class Color(Enum):
    RED = 1
    BLUE = 2


class Colored(Enum):
    RED = 1
    BLUE = 2


print(Colored.RED in [Color.RED]) # False
print(Colored.RED == Color.RED) # <= No unexpected True with == even if they both == to 1
print(Colored.RED.value == Color.RED.value)  # [enum-comparison-by-value]
print(Colored.RED.value in [Color.RED.value]) # [enum-comparison-by-value]

Imho if you're comparing enum by value explicitly (like if you're using IntEnum) chance are it's what you want to do.

@cdce8p
Copy link
Member

cdce8p commented Nov 29, 2021

After the discussion so far, I don't see a need for such a checker.

  • In case someone uses IntEnum or StrEnum they most likely what it to compare to ints and strings.
  • Same if someone is comparing Color.RED.value.
  • As for Colored.RED == Color.RED, that will already be false since enums are singletons.

@scop
Copy link
Contributor Author

scop commented Nov 29, 2021

In case someone uses IntEnum or StrEnum they most likely what it to compare to ints and strings.

They can do so by actually comparing to ints and strings, and in that case == is the right thing to do. But it's different from comparing to other enum members, in that case is is the right thing to do. As said earlier, I now think this check should be limited to cases where an enum member is compared against an enum member. So it should not be done when one is compared against something else (such as a string or an int).

Same if someone is comparing Color.RED.value.

This is out of scope here, .value is not an enum member but its value, which is often a str or an int.

As for Colored.RED == Color.RED, that will already be false since enums are singletons.

As noted in earlier comments here, this does not hold universally.

@scop
Copy link
Contributor Author

scop commented Nov 29, 2021

As for Colored.RED == Color.RED, that will already be false since enums are singletons.

As noted in earlier comments here, this does not hold universally.

There actually wasn't an explicit example of it, so here goes. Example using an IntEnum:

>>> from enum import IntEnum
>>> class Enum1(IntEnum):
...  FOO = 1
... 
>>> class Enum2(IntEnum):
...  BAR = 1
... 
>>> Enum1.FOO == Enum2.BAR
True
>>> Enum1.FOO is Enum2.BAR
False

And custom enums may be similarly affected:

>>> from enum import Enum
>>> class Enum3(str, Enum):
...  QUUX = "x"
... 
>>> class Enum4(str, Enum):
...  BAZ = "x"
... 
>>> Enum3.QUUX == Enum4.BAZ
True
>>> Enum3.QUUX is Enum4.BAZ
False

@cdce8p
Copy link
Member

cdce8p commented Nov 29, 2021

As for Colored.RED == Color.RED, that will already be false since enums are singletons.

As noted in earlier comments here, this does not hold universally.

There actually wasn't an explicit example of it, so here goes. Example using an IntEnum:

...
And custom enums may be similarly affected:
...

As mentioned, that is by design. If you don't want that, just use a normal enum.

@scop
Copy link
Contributor Author

scop commented Nov 29, 2021

Ok, let's stop the "as said" circle, it doesn't seem to be going anywhere.

One final try/question: do you think the advice at https://docs.python.org/3/library/enum.html#comparisons is misguided, or disagree with it otherwise? They are very explicit about it (emphasis mine), it was also partially cited in the initial comment in this issue, just in case it was missed:

Enumeration members are compared by identity:

>>> Color.RED is Color.RED
True
>>> Color.RED is Color.BLUE
False
>>> Color.RED is not Color.BLUE
True

If yes, can I ask to submit a PR to fix it or file a bug about it?

@Pierre-Sassoulas
Copy link
Member

Enumeration members are compared by identity:

The doc also says just after that : Equality comparisons are defined though. There is a lot of working code that use equality comparison for enum and the fail case we identified look like the user would know exactly what they are doing is they inherit from IntEnum or str, Enum or compare by value.

Addressing the main point here:

As far as opinionatedness goes, we do have singleton-comparison enabled by default already

The issue with comparing to True, False or None with equality means that a custom __eq__ will make the result uncertain. But the problem happens with every classes that you can compare it to (depending on the instance you get, you'll have problem or not) in particular it depends on code you do not control. When you're comparing enum with equality you control the enum class and there won't be a special case for your enum class in the instance you compare it too unless it's a class you control and you did this particular equality operator yourself. To be very specific numpy will not be changing their equality operator specifically for your enum class. But they did for True, False and None. So there's known case where singleton-comparison will create problems, and a lot less uncertainty with enums.

I'm not opposed to an optional extension, we already have an extension to prevent the use of while loop and enforce ternary operators so we're not opposed to having opinionated extensions.

@Pierre-Sassoulas Pierre-Sassoulas added Good first issue Friendly and approachable by new contributors Help wanted 🙏 Outside help would be appreciated, good for new contributors and removed Discussion 🤔 labels Nov 30, 2021
@scop
Copy link
Contributor Author

scop commented Nov 30, 2021

When you're comparing enum with equality you control the enum class

The enum classes whose members one compares (no matter how) can be defined in 3rd party code, outside one's control.

@cdce8p cdce8p removed this from the 2.13.0 milestone Dec 1, 2021
@Pierre-Sassoulas Pierre-Sassoulas added the Needs PR This issue is accepted, sufficiently specified and now needs an implementation label Jun 24, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Checkers Related to a checker Enhancement ✨ Improvement to a component Good first issue Friendly and approachable by new contributors Hacktoberfest Help wanted 🙏 Outside help would be appreciated, good for new contributors Needs PR This issue is accepted, sufficiently specified and now needs an implementation
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants