How to check if string exists in Enum of strings?
Asked Answered
P

15

36

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'
Presa answered 10/8, 2020 at 7:21 Comment(2)
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.Presa
I've reopened this question. It's about strings, and doesn't restrict try/catchBlende
E
48

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
Erzurum answered 9/12, 2020 at 22:19 Comment(5)
Very clean solution and achieves exactly what I wanted by being able to use the in keyword. Thanks!Presa
EnumMeta is undefined?Keening
@alphabetasoup: thx, I had implied the import. Edited the answer to make that explicit.Erzurum
Is EnumMeta really needed? Can't you define __contains__ in BaseEnum? It should work the same I guess.Sue
@Qback: don't guess, try ;-) Yes, the need for EnumMeta is explained very well in the first answer I linked in my post and that gave me the idea for this approach.Erzurum
B
22

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 guaranteed 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
Blende answered 10/8, 2020 at 7:52 Comment(0)
E
8

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
Estimative answered 31/3, 2021 at 15:53 Comment(5)
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?Silversmith
Did you dare trying it before writing your comment? This code works!Estimative
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.Silversmith
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.Estimative
@ShadySmaoui the op asked about an enum class where the value is different from the key. Your solution works for your enum class, but not for the op's, so it's not a correct solution to the question.Resigned
S
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)
Seedy answered 10/8, 2020 at 8:15 Comment(0)
T
4

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

Taciturn answered 30/3, 2023 at 21:41 Comment(1)
This is an excellent answer if you define actions like actions = [a.name for a in Action] Deucalion
H
3

With Python 3.12 you can just do

from enum import StrEnum

class Action(StrEnum):
    NEW_CUSTOMER = "new_customer"
    LOGIN = "login"
    BLOCK = "block"

and then check if a string is part of your Enum using in

>>> "login" in Action
True
>>> "logout" in Action
False
Happening answered 9/12, 2023 at 8:46 Comment(1)
wow. I'm surprised this didn't make the what's new docs, and is buried in the changelog. How did you discover this? I guess I need to pay more attention to the changelogs.Elle
W
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")
Westbrook answered 14/6, 2022 at 14:21 Comment(3)
Pardon me. Will delete the commentary to mislead no one. But still, this answer may be confusing.Sepulture
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"Degrading
I specifically stated "... if you can get Enum member name with what you have."Westbrook
C
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'
Chung answered 7/7, 2022 at 7:52 Comment(0)
U
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.

Uptrend answered 6/10, 2022 at 13:52 Comment(0)
C
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
Crepe answered 3/6, 2023 at 18:37 Comment(0)
W
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
Windflower answered 3/6, 2023 at 18:56 Comment(0)
Y
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']

Youmans answered 27/6, 2023 at 15:8 Comment(0)
V
0

I was a bit fustrated about this post as there are so many different examples of methods shown that didn't work so I grabbed Pytest and made som testing with the answers provided here

TL;DR try/catch is the only method that works in for all my testcases.

Here is the tests i made. I also wrote information about how they performed.

Doing testing I used the following test data:

  • Enum with same name as value,
  • Enum with different name for value
  • value with same name but one is upper and other is lower case

These data was then dublicated so they was firt a Enum object and second test row was just plain strings

Results:

  • try/catch passes all tests
  • hasattr only works for enum key=enum value
  • __members__ return a mappingproxy type which cannot be compared(it seems) since all tests fai
  • _value2member_map_ returns a dict where value is first and name is last. This means providing a string will only work when comparing and providing for example MyEnum.value will not work.
  • __contains__ example is hard to use and in my case i used OPs example which example does not work but implementing a contains metaclass does make for the cleanest solution which Pierre D and a few others provided.

Code used for testing:

from enum import Enum, EnumMeta
import pytest


class Result(Enum):
    PASS1 = "ThisPassed"
    PASS = "PASS"
    LOWERED_CASE = "lowered case"
    SAME_NAME = "SAME_NAME"


NOT_IN_RESULT_ENUM = str(Result.PASS.name) + "abcd"

test_parameters = [
    *[e for e in Result],  # All values here are of the type Result
    *[str(e.value) for e in Result],  # These values will be actual strings
]


def compare_try_except(item):
    try:
        return bool(Result(item))
    except ValueError:
        return False


@pytest.mark.parametrize("item", test_parameters)
def test_contains_in_enum(item):
    """
    This example uses the original method provided by user1023102 which is incorrectly implemented at least to my
    knowledge as it never converts the items in the enum to a string tha can be compared.
    I assume here goal was to convert it into a i.value fr i in self

    it is very complicated to use a you have to override the enum meta class with another class but it does look
    clean in use if done right!
    """

    class EnumContainsMeta(EnumMeta):

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

    class ResultWithContains(Enum, metaclass=EnumContainsMeta):
        PASS1 = "ThisPassed"
        PASS = "PASS"
        LOWERED_CASE = "lowered case"
        SAME_NAME = "SAME_NAME"

    assert (item in ResultWithContains) is True
    assert (NOT_IN_RESULT_ENUM in ResultWithContains) is False


@pytest.mark.parametrize("item", test_parameters)
def test__value2member_map_(item):
    """ Here all values that are string will pass. Using object from enum class will fail"""
    test = Result._value2member_map_
    assert item in Result._value2member_map_
    assert NOT_IN_RESULT_ENUM not in Result._value2member_map_


@pytest.mark.parametrize("item", test_parameters)
def test_members(item):
    """ This will not work as it will only work with members that share the same name
    and is not a object from the enum"""
    test = Result.__members__
    assert item in Result.__members__
    assert NOT_IN_RESULT_ENUM not in Result.__members__


@pytest.mark.parametrize("item", test_parameters)
def test_try_except(item):
    """ Passes all the tests """
    assert compare_try_except(item) is True
    assert compare_try_except(NOT_IN_RESULT_ENUM) is False


@pytest.mark.parametrize("item", test_parameters)
def test_hasattr(item):
    """ Only works if enum name and value is the same"""
    assert hasattr(Result, item) is True
    assert hasattr(Result, NOT_IN_RESULT_ENUM) is False

I hope my testing can help others in this post. And imo this is just fustrating having to use the try/except method. It should be changed so the enum could use "val1" in MyEnum and MyEnum.val1 in MyEnum.

Vestiary answered 10/12, 2023 at 14:46 Comment(0)
F
0

For some reason, I was getting the ValueError exception for the accepted answer with Python 3.9.15. So, I modified it this way:

  class MetaEnum(EnumMeta):
    """
    Meta class to define common enums
    """

    def __contains__(cls, member: Any):
        retVal: bool

        if isinstance(member, cls):
            retVal = EnumMeta.__contains__(cls, member)
        else:
            try:
                cls(member)  # pylint: disable = E1120
            except ValueError:
                retVal = member in cls.__members__.keys()

        return retVal
Financial answered 17/7 at 16:19 Comment(0)
P
-1

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")
Perspiratory answered 17/11, 2021 at 9:34 Comment(1)
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.Sepulture

© 2022 - 2024 — McMap. All rights reserved.