66

Since Python 3.4, the Enum class exists.

I am writing a program, where some constants have a specific order and I wonder which way is the most pythonic to compare them:

class Information(Enum):
    ValueOnly = 0
    FirstDerivative = 1
    SecondDerivative = 2

Now there is a method, which needs to compare a given information of Information with the different enums:

information = Information.FirstDerivative
print(value)
if information >= Information.FirstDerivative:
    print(jacobian)
if information >= Information.SecondDerivative:
    print(hessian)

The direct comparison does not work with Enums, so there are three approaches and I wonder which one is preferred:

Approach 1: Use values:

if information.value >= Information.FirstDerivative.value:
     ...

Approach 2: Use IntEnum:

class Information(IntEnum):
    ...

Approach 3: Not using Enums at all:

class Information:
    ValueOnly = 0
    FirstDerivative = 1
    SecondDerivative = 2

Each approach works, Approach 1 is a bit more verbose, while Approach 2 uses the not recommended IntEnum-class, while and Approach 3 seems to be the way one did this before Enum was added.

I tend to use Approach 1, but I am not sure.

Thanks for any advise!

juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172
Sebastian Werk
  • 1,568
  • 2
  • 17
  • 30
  • Could you quote that "*not recommended* IntEnum-class", please ? Docs for 3.7.1 are not deprecating it at all. – Patrizio Bertoni Nov 06 '18 at 08:45
  • 2
    Sure, from the Docs: “For the majority of new code, Enum and Flag are strongly recommended, since IntEnum and IntFlag break some semantic promises of an enumeration (by being comparable to integers, and thus by transitivity to other unrelated enumerations). IntEnum and IntFlag should be used only in cases where Enum and Flag will not do; for example, when integer constants are replaced with enumerations, or for interoperability with other systems.” – Sebastian Werk Nov 06 '18 at 15:19
  • Approach 1 works for me thanks! – LCoelho Oct 27 '19 at 15:17

5 Answers5

72

You should always implement the rich comparison operaters if you want to use them with an Enum. Using the functools.total_ordering class decorator, you only need to implement an __eq__ method along with a single ordering, e.g. __lt__. Since enum.Enum already implements __eq__ this becomes even easier:

>>> import enum
>>> from functools import total_ordering
>>> @total_ordering
... class Grade(enum.Enum):
...   A = 5
...   B = 4
...   C = 3
...   D = 2
...   F = 1
...   def __lt__(self, other):
...     if self.__class__ is other.__class__:
...       return self.value < other.value
...     return NotImplemented
... 
>>> Grade.A >= Grade.B
True
>>> Grade.A >= 3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unorderable types: Grade() >= int()

Terrible, horrible, ghastly things can happen with IntEnum. It was mostly included for backwards-compatibility sake, enums used to be implemented by subclassing int. From the docs:

For the vast majority of code, Enum is strongly recommended, since IntEnum breaks some semantic promises of an enumeration (by being comparable to integers, and thus by transitivity to other unrelated enumerations). It should be used only in special cases where there’s no other choice; for example, when integer constants are replaced with enumerations and backwards compatibility is required with code that still expects integers.

Here's an example of why you don't want to do this:

>>> class GradeNum(enum.IntEnum):
...   A = 5
...   B = 4
...   C = 3
...   D = 2
...   F = 1
... 
>>> class Suit(enum.IntEnum):
...   spade = 4
...   heart = 3
...   diamond = 2
...   club = 1
... 
>>> GradeNum.A >= GradeNum.B
True
>>> GradeNum.A >= 3
True
>>> GradeNum.B == Suit.spade
True
>>> 
juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172
  • 2
    Great description, thanks a lot. Just one question: You `return NotImplemented` instead of `raise NotImplemented`. Is there a general rule, when to use return and when raise? – Sebastian Werk Sep 14 '16 at 06:53
  • 4
    @SebastianWerk Well, you cannot `raise NotImplemented` because it is not an exception. It is a built-in singleton. See the [docs](https://docs.python.org/3.5/library/constants.html#NotImplemented), it is there for the special case of the rich-comparison operators. The `NotImplementedError`, according to the [docs](https://docs.python.org/2/library/exceptions.html#exceptions.NotImplementedError), is there for when "abstract methods should raise this exception when they require derived classes to override the method. ". – juanpa.arrivillaga Sep 14 '16 at 07:26
  • 2
    @SebastianWerk Also, see this question: http://stackoverflow.com/questions/878943/why-return-notimplemented-instead-of-raising-notimplementederror – juanpa.arrivillaga Sep 14 '16 at 07:26
  • 6
    **Excellent answer,** good Sir. This approach is a succinct – albeit less efficient – alternative to the `OrderedEnum` class detailed in the [official Python documentation](https://docs.python.org/3/library/enum.html#orderedenum). While the `OrderedEnum` solution of manually implementing all comparison operators _is_ modestly faster, the `@total_ordering` solution given above has its merits. Brevity is a thankless virtue. Relatedly, does anyone know why `OrderedEnum` was merely documented rather than added to the `enum` module? – Cecil Curry Dec 02 '16 at 06:05
  • 2
    Is there a python `Enum` construct or similar that actually kinda .. _works_ ? Adding @total_ordering and implementing comparison operators is _not_ something should have to code ourselves. This is too much boilerplate. – WestCoastProjects Feb 10 '20 at 07:31
  • It seems you are implementing the `__lt__` (i.e. less than) method but in your example you are performing a greater than (or equal to) comparison. How does that work? Is it because you are comparing Grade against Grade and therefore the direction of comparison is relative? Does that mean that if you are only going to perform symmetric comparisons it's enough to only implement one of the `__gt__` / `__lt__` - methods of your own choosing? – Nermin Apr 07 '23 at 08:00
  • @Nermin no, it's because I used the [`@functools.total_ordering` decorator](https://docs.python.org/3/library/functools.html#functools.total_ordering) – juanpa.arrivillaga Apr 07 '23 at 08:14
  • I implemented only the `__gt__` method instead of `__lt__` and it worked the same. – Nermin Apr 07 '23 at 08:53
  • @Nermin yes, again, that works because I used the [`@functools.total_ordering` decorator](https://docs.python.org/3/library/functools.html#functools.total_ordering). Did you read the linked documentation? – juanpa.arrivillaga Apr 07 '23 at 08:55
23

I hadn't encountered Enum before so I scanned the doc (https://docs.python.org/3/library/enum.html) ... and found OrderedEnum (section 8.13.13.2) Isn't this what you want? From the doc:

>>> class Grade(OrderedEnum):
...     A = 5
...     B = 4
...     C = 3
...     D = 2
...     F = 1
...
>>> Grade.C < Grade.A
True
Gulzar
  • 23,452
  • 27
  • 113
  • 201
nigel222
  • 7,582
  • 1
  • 14
  • 22
  • This approach doesn't work due to unfixed Python bug: https://bugs.python.org/issue30545 You should always override comparator methods like __eq__ and __lt__ – asu Oct 04 '19 at 22:05
  • 1
    @asu there is no unfixed bug, the issues in that thread are not a problem with `Enum` itself, and in any case, this example **does** override the comparison operators. – juanpa.arrivillaga Oct 28 '19 at 17:34
  • 6
    Can't seem to import in Python 3.6, `ImportError: cannot import name 'OrderedEnum'`? **EDIT:** It looks like this is an "Interesting Example" and not actually in the standard python library. You'd need to copy the snippet from the docs to use it. – Cobertos Jan 31 '20 at 02:29
  • 1
    The documentation for OrderedEnum has been moved to here: https://docs.python.org/3/howto/enum.html#orderedenum – Nermin Apr 07 '23 at 07:43
2

You can create a simple decorator to resolve this too:

from enum import Enum
from functools import total_ordering

def enum_ordering(cls):
    def __lt__(self, other):
        if type(other) == type(self):
            return self.value < other.value

        raise ValueError("Cannot compare different Enums")

    setattr(cls, '__lt__', __lt__)
    return total_ordering(cls)


@enum_ordering
class Foos(Enum):
    a = 1
    b = 3
    c = 2

assert Names.a < Names.c
assert Names.c < Names.b
assert Names.a != Foos.a
assert Names.a < Foos.c # Will raise a ValueError

For bonus points you could implement the other methods in @VoteCoffee's answer above

sam
  • 1,005
  • 1
  • 11
  • 24
1

Combining some of the above ideas, you can subclass enum.Enum to make it comparable to string/numbers and then build your enums on this class instead:

import numbers
import enum


class EnumComparable(enum.Enum):
    def __gt__(self, other):
        try:
            return self.value > other.value
        except:
            pass
        try:
            if isinstance(other, numbers.Real):
                return self.value > other
        except:
            pass
        return NotImplemented

    def __lt__(self, other):
        try:
            return self.value < other.value
        except:
            pass
        try:
            if isinstance(other, numbers.Real):
                return self.value < other
        except:
            pass
        return NotImplemented

    def __ge__(self, other):
        try:
            return self.value >= other.value
        except:
            pass
        try:
            if isinstance(other, numbers.Real):
                return self.value >= other
            if isinstance(other, str):
                return self.name == other
        except:
            pass
        return NotImplemented

    def __le__(self, other):
        try:
            return self.value <= other.value
        except:
            pass
        try:
            if isinstance(other, numbers.Real):
                return self.value <= other
            if isinstance(other, str):
                return self.name == other
        except:
            pass
        return NotImplemented

    def __eq__(self, other):
        if self.__class__ is other.__class__:
            return self == other
        try:
            return self.value == other.value
        except:
            pass
        try:
            if isinstance(other, numbers.Real):
                return self.value == other
            if isinstance(other, str):
                return self.name == other
        except:
            pass
        return NotImplemented
VoteCoffee
  • 4,692
  • 1
  • 41
  • 44
0

for those who want to use the == with two enum instances like that: enum_instance_1 == enum_instance_2

just add the __eq__ method in your Enum class as follows:

def __eq__(self, other):
    return self.__class__ is other.__class__ and other.value == self.value
Hassan Kanso
  • 237
  • 1
  • 10
  • This is totally unnecessary... this `==` behavior **already exists** for instances of classes that inherit from `enum.Enum` or use `enum.EnumMeta` as their metaclass. Note, you can also just use `is` to compare enums! – juanpa.arrivillaga Jun 05 '23 at 21:10