UPDATE: One other thing to consider, which I think I like better, on balance, is to define subset membership as an attribute of each Enum member, like:
fruit = FRUIT.ORANGE # ---or whatever, probably in far away code--- ... if fruit.is_citrus: make_juice()
These can be defined as @propertys on the class and don't suffer from the mutability problems mentioned below.
class FRUIT(Enum): APPLE = 1 BANANA = 2 LEMON = 3 ORANGE = 4 @property def is_citrus(self): return self in frozenset((FRUIT.LEMON, FRUIT.ORANGE))
Thanks to the other respondents who all contributed very useful points of view. Here's what I ended up doing after considering the other answers, followed by my rationale:
from enum import Enum class FRUIT(Enum): APPLE = 1 BANANA = 2 LEMON = 3 ORANGE = 4 FRUIT.CITRUS_TYPES = frozenset((FRUIT.LEMON, FRUIT.ORANGE))
This works fine and (suprisingly to me) doesn't break any of the other Enum behaviors:
# ---CITRUS_TYPES subset has desired behavior--- >>> FRUIT.LEMON in FRUIT.CITRUS_TYPES True >>> FRUIT.APPLE in FRUIT.CITRUS_TYPES False >>> "foobar" in FRUIT.CITRUS_TYPES False # ---CITRUS_TYPES has not become a member of FRUIT enum--- >>> tuple(FRUIT) (FRUIT.APPLE: 1>, <FRUIT.BANANA: 2>, <FRUIT.LEMON: 3>, <FRUIT.ORANGE: 4>) >>> FRUIT.APPLE in FRUIT True >>> FRUIT.CITRUS_TYPES in FRUIT DeprecationWarning: using non-Enums in containment checks will raise TypeError in Python 3.8 False # ---CITRUS_TYPES not reported by dir(FRUIT)--- >>> dir(FRUIT) ['APPLE', 'BANANA', 'LEMON', 'ORANGE', '__class__', '__doc__', '__members__', '__module__'] # ---But it does appear on FRUIT.__dict__--- FRUIT.__dict__ == { '_generate_next_value_': <function Enum._generate_next_value_ at 0x1010e9268>, '__module__': '__main__', '__doc__': 'An enumeration.', '_member_names_': ['APPLE', 'BANANA', 'LEMON', 'ORANGE'], '_member_map_': OrderedDict([ ('APPLE', <FRUIT.APPLE: 1>), ('BANANA', <FRUIT.BANANA: 2>), ('LEMON', <FRUIT.LEMON: 3>), ('ORANGE', <FRUIT.ORANGE: 4>) ]), '_member_type_': <class 'object'>, '_value2member_map_': { 1: <FRUIT.APPLE: 1>, 2: <FRUIT.BANANA: 2>, 3: <FRUIT.LEMON: 3>, 4: <FRUIT.ORANGE: 4>, }, 'APPLE': <FRUIT.APPLE: 1>, 'BANANA': <FRUIT.BANANA: 2>, 'LEMON': <FRUIT.LEMON: 3>, 'ORANGE': <FRUIT.ORANGE: 4>, '__new__': <function Enum.__new__ at 0x1010e91e0>, 'CITRUS_TYPES': frozenset({<FRUIT.LEMON: 3>, <FRUIT.ORANGE: 4>}) }
So it appears to store CITRUS_TYPES on the class, but hides it from dir() for whatever reason.
It does have a vulnerability though, in that the added attribute is mutable, like any other class attribute; if some part of the client code assigns to FRUIT.CITRUS_TYPES, FRUIT would not complain and that would of course break things. This behavior is unlike that of an Enum member, which raises AttributeError on attempted assignment.
I thought this might be remedied by making it a classproperty, which I ended up trying, but my early attempts did not prevent mutation. A more sophisticated classproperty implementation described there might work, but I ended up be satisfied with the simple approach above for now.
@blhsing raises an interesting question on whether such an attribute on FRUIT makes sense. I see his point, and may yet adopt his view, but my current view is that localizing "fruit-related" characteristics to a single imported name is best for me. One could consider FRUIT a rigorous set of fruit-types and subsets of fruit to therefore be a distinct set. I find that rigor unrewarding for my current purposes and prefer to think of FRUIT as more of a collection of related constant values, including both members and subsets. YMMV of course. Like I say, I may yet adopt his view.
Enumbuilt-in methods, which operate on all attributes at that level.