26

I have created the following Enum:

from enum import Enum

class Action(str, Enum):
    NEW_CUSTOMER = "new_customer"
    LOGIN = "login"
    BLOCK = "block"

I have inherited from str, too, so that I can do things such as:

action = "new_customer"
...
if action == Action.NEW_CUSTOMER:
    ...

I would now like to be able to check if a string is in this Enum, such as:

if "new_customer" in Action:
    ....

I have tried adding the following method to the class:

def __contains__(self, item):
    return item in [i for i in self]

However, when I run this code:

print("new_customer" in [i for i in Action])
print("new_customer" in Action)

I get this exception:

True
Traceback (most recent call last):
  File "/Users/kevinobrien/Documents/Projects/crazywall/utils.py", line 24, in <module>
    print("new_customer" in Action)
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/enum.py", line 310, in __contains__
    raise TypeError(
TypeError: unsupported operand type(s) for 'in': 'str' and 'EnumMeta'
KOB
  • 4,084
  • 9
  • 44
  • 88
  • No, I have shown that I can do something like `"new_customer" in [i for i in Action]`, but I want something cleaner such as `"new_customer" in Action`. – KOB Aug 10 '20 at 07:44
  • 1
    I've reopened this question. It's about strings, and doesn't restrict try/catch – Mad Physicist Aug 10 '20 at 13:36

12 Answers12

33

I just bumped into this problem today (2020-12-09); I had to change a number of subpackages for Python 3.8.

Perhaps an alternative to the other solutions here is the following, inspired by the excellent answer here to a similar question, as well as @MadPhysicist's answer on this page:

from enum import Enum, EnumMeta


class MetaEnum(EnumMeta):
    def __contains__(cls, item):
        try:
            cls(item)
        except ValueError:
            return False
        return True    


class BaseEnum(Enum, metaclass=MetaEnum):
    pass


class Stuff(BaseEnum):
    foo = 1
    bar = 5

Tests (python >= 3.7; tested up to 3.10):

>>> 1 in Stuff
True

>>> Stuff.foo in Stuff
True

>>> 2 in Stuff
False

>>> 2.3 in Stuff
False

>>> 'zero' in Stuff
False
Pierre D
  • 24,012
  • 7
  • 60
  • 96
  • Very clean solution and achieves exactly what I wanted by being able to use the `in` keyword. Thanks! – KOB Dec 10 '20 at 05:34
  • `EnumMeta` is undefined? – alphabetasoup Jun 02 '22 at 02:31
  • @alphabetasoup: thx, I had implied the `import`. Edited the answer to make that explicit. – Pierre D Jun 02 '22 at 17:27
  • 1
    Is `EnumMeta` really needed? Can't you define `__contains__` in `BaseEnum`? It should work the same I guess. – Qback Dec 14 '22 at 22:59
  • 1
    @Qback: don't guess, try ;-) Yes, the need for `EnumMeta` is explained very well in the first [answer](https://stackoverflow.com/a/10446010/758174) I linked in my post and that gave me the idea for this approach. – Pierre D Aug 30 '23 at 11:34
19

You can check if the enum contains a value by calling it:

>>> Action('new_customer')
Action.NEW_CUSTOMER

If the object you pass in is not guarantee to be in the enum, you can use a try block to capture the resulting ValueError. E.g.,

def is_action(obj):
    try:
        Action(obj)
    except ValueError:
        return False
    return True
Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
10

Given an enum of languages

class Language(enum.Enum):
    en = 'en'
    zh = 'zh'

    @classmethod
    def has_member_key(cls, key):
        return key in cls.__members__

print(Language.has_member_key('tu')) => False
print(Language.has_member_key('en')) => True
Shady Smaoui
  • 867
  • 9
  • 11
  • 2
    This code doesn't work because your checking string not to 'en', 'zh' string values but enum key names. Leave values and change enum key names to English, Chinese and then try if your code works? – s.paszko Feb 07 '23 at 09:59
  • Did you dare trying it before writing your comment? This code works! – Shady Smaoui Feb 07 '23 at 19:57
  • 2
    Yes, your code works by accident, because of identical enum key names and key values. As i've said try diffrent version with keys renamed. Try this enum with your method : ` class Language(Enum): English = 'en' Chinese = 'zh' ` Question was about checking enum str values inside enum, not enum key names. – s.paszko Feb 08 '23 at 14:54
  • It works for this specific case. A code is written by definition to solve a specific problem! So it's not by accident. It obviously won't work if you have a string value different from the key. – Shady Smaoui Mar 01 '23 at 05:05
  • In the real world, code isn't a static entity. It grows over time. Perhaps the next developer that touches this block doesn't realize the hidden requirement of "name of enums in Language MUST be equivalent to their string representation for this class to work as expected". This is how bugs are made. – Eric Le Fort Jun 03 '23 at 17:59
6

Since Action is a derived class of Enum, we can use the fact that Enum has a member called _value2member_map_.

value2member_map is a private attribute (i.e. Internally in CPython) tthat maps values to names(will only work for hashable values though). However, it's not a good idea to rely on private attributes as they can be changed anytime.

Reference

We get the following:

if "new_customer" in Action._value2member_map_:  # works

which is close to your desired:

if "new_customer" in Action:  # doesn't work (i.e. TypeError)
DarrylG
  • 16,732
  • 2
  • 17
  • 23
1

You can use hasattr instead of in if you can get Enum member name with what you have.

action = "new_customer"
enum_member_name = action.upper()

if hasattr(Action, enum_member_name):
    print(f"{action} in Action")
else:
    print(f"{action} not in Action")
alercelik
  • 615
  • 7
  • 11
  • Pardon me. Will delete the commentary to mislead no one. But still, this answer may be confusing. – egvo Sep 27 '22 at 06:15
  • no, this works only for a very special case. the questions asks how to check the values not the keys. yours doesn't work in this: `NEW_CUSTOMER = "newcustomer"` – yasin_alm Jul 28 '23 at 09:30
  • I specifically stated "... if you can get Enum member name with what you have." – alercelik Aug 07 '23 at 12:25
0

You can also check contains in enum by brackets like in dict

class Action(Enum):
    NEW_CUSTOMER = 1
    LOGIN = 2
    BLOCK = 3

action = 'new_customer'
try:
    action = Action[action.upper()]
    print("action type exists")
except KeyError:
    print("action type doesn't exists")
Pavivin
  • 11
  • 2
  • You should note, that you check not a value, but a key of enum. In the question it's clear that author checks value. No downvoting just because it may be helpful for others. – egvo Sep 26 '22 at 07:29
0

I generally use following code to have both functionality:

  1. 'service' in ENTITY_TYPES
  2. ENTITY_TYPES.SERVICE in ENTITY_TYPES
from enum import Enum, EnumMeta
from typing import Any

class EnumeratorMeta(EnumMeta):

    def __contains__(cls, member: Any):
        if type(member) == cls:
            return EnumMeta.__contains__(cls, member)
        else:
            try:
                cls(member)
            except ValueError:
                return False
            return True


class Enumerator(Enum, metaclass=EnumeratorMeta):
    pass


class ENTITY_TYPES(Enumerator):
    SERVICE: str = 'service'
    CONFIGMAP: str = 'configmap'
0

I want to keep my enum classes generic (without altering internal functionality):

def clean_class_dict(class_dict):
    return_dict = dict(class_dict)
    for key in list(return_dict.keys()):
        if key[0] == "_":
            del return_dict[key]
    return return_dict

def item_in_enum_titles(item: str, enum: Enum):
    enum_dict = clean_class_dict(enum.__dict__)
    if item in enum_dict.keys():
        return True
    else:
        return False

I convert my enum to a dict and remove all the private functions and variables.

bieboebap
  • 320
  • 3
  • 18
0

Here's a simple way to check if a string value belongs to an Enum. Just thought I'd include it here for completeness:

from enum import Enum

class Action(Enum):
    NEW_CUSTOMER = "new_customer"
    LOGIN = "login"

actions = [a.value for a in Action]

# client code
returns_true = 'login' in actions
returns_false = 'Not an action' in actions

DV82XL
  • 5,350
  • 5
  • 30
  • 59
0

TL;DR _value2member_map_, while a little hacky due to private access, is ~8x faster than the try-cast approaches here.

I was curious (and know ops like casting and error handling are slow) so I did a quick performance analysis on a few of the top answers. Thought I'd share my results for posterity.

import timeit
from enum import Enum, EnumMeta
from random import randint


class MetaEnum(EnumMeta):
    def __contains__(cls, item):
        try:
            cls(item)
        except ValueError:
            return False
        return True    


class BaseEnum(Enum, metaclass=MetaEnum):
    pass


class Action(BaseEnum):
    A = 1
    B = 2
    C = 3
    D = 4
    
    def is_action(obj):
        try:
            Action(obj)
        except ValueError:
            return False
        return True

repeat, N = 100, 10000
t_is_x = timeit.repeat(stmt="Action.is_action(i)", setup='from random import randint; i = randint(1, 8)', number=N, repeat=repeat, globals=globals())
t_meta = timeit.repeat(stmt="i in Action", setup='from random import randint; i = randint(1, 8)', number=N, repeat=repeat, globals=globals())
t_valuemap = timeit.repeat(stmt="i in Action._value2member_map_", setup='from random import randint; i = randint(1, 8)', number=N, repeat=repeat, globals=globals())

print(f"Time for is_x: {min(t_is_x)}")
print(f"Time for meta: {min(t_meta)}")
print(f"Time for value map: {min(t_valuemap)}")
Time for is_x: 0.008271969389170408
Time for meta: 0.007943496108055115
Time for value map: 0.0010849367827177048
Eric Le Fort
  • 2,571
  • 18
  • 24
0

My code involves a few enums being used to represent state and being passed between different languages, so I wrote something like this for the Python part and the other language separately, so that I can safely convert to JSON and back either way. This is just representational though.

// Step 1: Define the enums
Service = Enum('Service', ['Plumbing', 'Electrical', 'Carpentry', 'Special'])
Plumbing = Enum('Plumbing', ['REGULAR', 'EXPRESS'])
Electrical = Enum('Electrical', ['REGULAR', 'REWIRING', 'NEWSOCKETS'])
Carpentry = Enum('Carpentry', ['REPAIR', 'NEW'])
Special = Enum('Special', ['DEEPCLEAN', 'TOILETS'])

    
// step 2, define a dict to keep a track of your enums
enumlist = {
    'Plumbing' : Plumbing,
    'Electrical' : Electrical,
    'Carpentry' : Carpentry,
    'Special' : Special
}


// step 3 : functions to convert an enum to and from string   
def str_to_enum(namestring):
    try:
        if namestring.split(".", 2)[0] not in enumlist:
            return None
        return enumlist[namestring.split(".", 2)[0]][namestring.split(".", 2)[1]]
    except KeyError as e:
        return None
        
def enum_to_tr(value):
    if not isinstance(value, Enum):
        return None
    return str(value)

// step 4, a function to check if the enum named has the key needed

def is_in(enumname, keystr):   
    supposedEnum = f'{enumname}.{keystr}'
    
    if str_to_enum(supposedEnum) is None:
        return False
    return True
E_net4
  • 27,810
  • 13
  • 101
  • 139
kilokahn
  • 1,136
  • 2
  • 18
  • 38
0

Here is a new shorter workaround I found (use it with an upper-case values for safety):

from enum import Enum

# The functional syntax of declaring an Enum:
Color = Enum('Color', ['RED', 'GREEN', 'BLUE'])

str_val = 'red'
print( str_val.upper() in dir(Color)[:3] )

The printed result is 'True'.'

dir(Color)[:3] is a returned list: ['BLUE','GREEN','RED']

Ofer Calvo
  • 159
  • 6