254

I wonder what's the correct way of converting (deserializing) a string to a Python's Enum class. Seems like getattr(YourEnumType, str) does the job, but I'm not sure if it's safe enough.

Just to be more specific, I would like to convert a 'debug'string to an Enum object like this:

class BuildType(Enum):
    debug = 200
    release = 400
Vadim Kotov
  • 7,766
  • 8
  • 46
  • 61
Vladius
  • 3,412
  • 2
  • 18
  • 18

8 Answers8

413

This functionality is already built in to Enum [1]:

>>> from enum import Enum
>>> class Build(Enum):
...   debug = 200
...   build = 400
... 
>>> Build['debug']
<Build.debug: 200>

The member names are case sensitive, so if user-input is being converted you need to make sure case matches:

an_enum = input('Which type of build?')
build_type = Build[an_enum.lower()]

[1] Official docs: Enum programmatic access

Ethan Furman
  • 57,917
  • 18
  • 142
  • 218
  • 11
    What about a fallback value in case the input needs to be sanitised? Something in the sort of `Build.get('illegal', Build.debug)`? – Hetzroni Feb 05 '18 at 10:19
  • 1
    @Hetzroni: `Enum` does not come with a `.get()` method, but you can add one as needed, or just make a base `Enum` class and always inherit from that. – Ethan Furman Feb 05 '18 at 16:29
  • 2
    @Hetzroni: Per the "ask for forgiveness, not for permission" principle, you can always envelop the access in a try/except KeyError clause to return the default (and as Ethan mentioned, optionally wrap this up in your own function/method). – Laogeodritt Feb 09 '18 at 22:37
  • Worth noting here - if using this for serialization / deserialization, serialize the `name` property for this, so use `Build.debug.name` rather than `str(Build.debug)` for this sort of lookup to work (otherwise it tries to find `Build.debug` on the deserialization side which won't exist). – fquinner Oct 09 '19 at 11:22
  • 9
    @Dragonborn It wouldn't work to call `Build('debug')`. The class constructor must take the *value*, i.e. `200` or `400` in this example. To pass the *name* you must use square brackets, as the answer already says. – Arthur Tacca Jan 20 '20 at 14:34
  • Is there any way to overwrite the `__getitem__` or some other built-in method for `"illegal"` to apply custom parsing? – ClementWalter Nov 04 '20 at 17:12
  • @ClementWalter: Sorry, your question is unclear. If you ask a new question with more details we can answer it. – Ethan Furman Nov 04 '20 at 18:19
  • This is related to other answers with a `from_str` that allows for some parsing instead of the use of the direct `__getitem__` method with `Build['debug']`. Is it possible to extend the `__getitem__` (or some other magicmethod` so that `Build["illegal"]` may return something? – ClementWalter Nov 04 '20 at 18:53
34

Another alternative (especially useful if your strings don't map 1-1 to your enum cases) is to add a staticmethod to your Enum, e.g.:

class QuestionType(enum.Enum):
    MULTI_SELECT = "multi"
    SINGLE_SELECT = "single"

    @staticmethod
    def from_str(label):
        if label in ('single', 'singleSelect'):
            return QuestionType.SINGLE_SELECT
        elif label in ('multi', 'multiSelect'):
            return QuestionType.MULTI_SELECT
        else:
            raise NotImplementedError

Then you can do question_type = QuestionType.from_str('singleSelect')

rogueleaderr
  • 4,432
  • 2
  • 32
  • 40
13

My Java-like solution to the problem. Hope it helps someone...

from enum import Enum, auto


class SignInMethod(Enum):
    EMAIL = auto(),
    GOOGLE = auto()

    @classmethod
    def value_of(cls, value):
        for k, v in cls.__members__.items():
            if k == value:
                return v
        else:
            raise ValueError(f"'{cls.__name__}' enum not found for '{value}'")


sim = SignInMethod.value_of('EMAIL')
assert sim == SignInMethod.EMAIL
assert sim.name == 'EMAIL'
assert isinstance(sim, SignInMethod)
# SignInMethod.value_of("invalid sign-in method")  # should raise `ValueError`
Mitch
  • 1,332
  • 1
  • 15
  • 17
12
def custom_enum(typename, items_dict):
    class_definition = """
from enum import Enum

class {}(Enum):
    {}""".format(typename, '\n    '.join(['{} = {}'.format(k, v) for k, v in items_dict.items()]))

    namespace = dict(__name__='enum_%s' % typename)
    exec(class_definition, namespace)
    result = namespace[typename]
    result._source = class_definition
    return result

MyEnum = custom_enum('MyEnum', {'a': 123, 'b': 321})
print(MyEnum.a, MyEnum.b)

Or do you need to convert string to known Enum?

class MyEnum(Enum):
    a = 'aaa'
    b = 123

print(MyEnum('aaa'), MyEnum(123))

Or:

class BuildType(Enum):
    debug = 200
    release = 400

print(BuildType.__dict__['debug'])

print(eval('BuildType.debug'))
print(type(eval('BuildType.debug')))    
print(eval(BuildType.__name__ + '.debug'))  # for work with code refactoring
ADR
  • 1,201
  • 9
  • 20
  • I mean I would like to convert a `debug` string to an enum of such: ```python class BuildType(Enum): debug = 200 release = 400 ``` – Vladius Dec 31 '16 at 11:29
  • Great tips! Is using `__dict__` the same as `getattr`? I'm worrying about name collisions with internal Python attributes.... – Vladius Dec 31 '16 at 12:38
  • Oh... yes it the same as `getattr`. I see no reason for name collisions. You just can't set keyword as field of class. – ADR Dec 31 '16 at 13:17
3

An improvement to the answer of @rogueleaderr :

class QuestionType(enum.Enum):
    MULTI_SELECT = "multi"
    SINGLE_SELECT = "single"

    @classmethod
    def from_str(cls, label):
        if label in ('single', 'singleSelect'):
            return cls.SINGLE_SELECT
        elif label in ('multi', 'multiSelect'):
            return cls.MULTI_SELECT
        else:
            raise NotImplementedError
Javed
  • 5,551
  • 3
  • 40
  • 66
1

Since MyEnum['dontexist'] will result in error KeyError: 'dontexist', you might like to fail silently (eg. return None). In such case you can use the following static method:

class Statuses(enum.Enum):
    Unassigned = 1
    Assigned = 2

    @staticmethod
    def from_str(text):
        statuses = [status for status in dir(
            Statuses) if not status.startswith('_')]
        if text in statuses:
            return getattr(Statuses, text)
        return None


Statuses.from_str('Unassigned')
nme
  • 1,015
  • 10
  • 8
0

Change your class signature to this:

class BuildType(str, Enum):
Robson
  • 786
  • 5
  • 22
  • 39
-2

I just want to notify this does not work in python 3.6

class MyEnum(Enum):
    a = 'aaa'
    b = 123

print(MyEnum('aaa'), MyEnum(123))

You will have to give the data as a tuple like this

MyEnum(('aaa',))

EDIT: This turns out to be false. Credits to a commenter for pointing out my mistake

Sstuber
  • 13
  • 3
  • Using Python 3.6.6, I could not reproduce this behaviour. I think you may have made a mistake while testing (I know I did the first time when checking this). If you accidentally put a `,` (comma) after each element (as if the elements were a list) then it treats each element as a tuple. (i.e. `a = 'aaa',` is actually the same as `a = ('aaa',)`) – Multihunter Oct 24 '18 at 04:06
  • You are right, It was different bug in my code. I Somehow thought you needed to put `,` behind every line while defining the enum which turned the values into tuples somehow – Sstuber Oct 24 '18 at 10:34