62

Consider:

class Item:
   def __init__(self, a, b):
       self.a = a
       self.b = b

class Items:
    GREEN = Item('a', 'b')
    BLUE = Item('c', 'd')

Is there a way to adapt the ideas for simple enums to this case? (see this question) Ideally, as in Java, I would like to cram it all into one class.

Java model:

enum EnumWithAttrs {
    GREEN("a", "b"),
    BLUE("c", "d");

    EnumWithAttrs(String a, String b) {
      this.a = a;
      this.b = b;
    }

    private String a;
    private String b;

    /* accessors and other java noise */
}
mkrieger1
  • 19,194
  • 5
  • 54
  • 65
bmargulies
  • 97,814
  • 39
  • 186
  • 310
  • 8
    What you are trying to do is not at all clear from the phrase "adapt the ideas for simple enums to this case". – kindall Oct 01 '12 at 19:54
  • 4
    Use a [named tuple](http://docs.python.org/library/collections.html#collections.namedtuple) perhaps? But then again, no, it's better to use a class for that instead. – Martijn Pieters Oct 01 '12 at 19:56
  • Seems to be a related question here: https://stackoverflow.com/questions/73750488/python-enum-with-multiple-attributes – NeilG Sep 01 '23 at 09:45
  • Also, for an optionally dynamically created `enum` which works with `pydantic` by name but supports a value of any type refer to the comprehensive answers here: https://stackoverflow.com/questions/75587442/validate-pydantic-dynamic-float-enum-by-name-with-openapi-description – NeilG Sep 01 '23 at 09:46

9 Answers9

62

Python 3.4 has a new Enum data type (which has been backported as enum34 and enhanced as aenum1). Both enum34 and aenum2 easily support your use case:

  • aenum (Python 2/3)

      import aenum
      class EnumWithAttrs(aenum.AutoNumberEnum):
          _init_ = 'a b'
          GREEN = 'a', 'b'
          BLUE = 'c', 'd'
    
  • enum34 (Python 2/3) or standard library enum (Python 3.4+)

      import enum
      class EnumWithAttrs(enum.Enum):
    
          def __new__(cls, *args, **kwds):
              value = len(cls.__members__) + 1
              obj = object.__new__(cls)
              obj._value_ = value
              return obj
          def __init__(self, a, b):
              self.a = a
              self.b = b
    
          GREEN = 'a', 'b'
          BLUE = 'c', 'd'
    

And in use:

>>> EnumWithAttrs.BLUE
<EnumWithAttrs.BLUE: 1>

>>> EnumWithAttrs.BLUE.a
'c'

1 Disclosure: I am the author of the Python stdlib Enum, the enum34 backport, and the Advanced Enumeration (aenum) library.

2 aenum also supports NamedConstants and metaclass-based NamedTuples.

Ethan Furman
  • 63,992
  • 20
  • 159
  • 237
44

For Python 3:

class Status(Enum):
    READY = "ready", "I'm ready to do whatever is needed"
    ERROR = "error", "Something went wrong here"

    def __new__(cls, *args, **kwds):
        obj = object.__new__(cls)
        obj._value_ = args[0]
        return obj

    # ignore the first param since it's already set by __new__
    def __init__(self, _: str, description: str = None):
        self._description_ = description

    def __str__(self):
        return self.value

    # this makes sure that the description is read-only
    @property
    def description(self):
        return self._description_

And you can use it as a standard enum or factory by type:

print(Status.READY)
# ready
print(Status.READY.description)
# I'm ready to do whatever is needed
print(Status("ready")) # this does not create a new object
# ready
Ovidiu S.
  • 541
  • 4
  • 3
  • 10
    It'd be nice to have an explanation of why `__new__` is required in this context since so may other classes don't require it. – Philip Couling Jun 22 '21 at 09:52
  • What if we need something like `class Status(str, Enum):`? I needed to change the line `obj = object.__new__(cls)` to `obj = str.__new__(cls)`, but now we have that `print(str(Status.READY == Status.ERROR))` results in " True". Any idea? – Eduardo Lucio Mar 09 '22 at 19:26
  • The `def __str__(self):` method is unnecessary. – Eduardo Lucio Mar 09 '22 at 19:31
  • In my humble view, extending str and enum at the same time is wrong. However, if you still want to do it you probably need to overload ```def __eq__(self, other):``` as the default str implementation differs from the enum class. – Ovidiu S. Mar 11 '22 at 09:12
39

Before Python 3.4 and the addition of the excellent enum module, a good choice would have been to use a namedtuple:

from collections import namedtuple

Item = namedtuple('abitem', ['a', 'b'])

class Items:
    GREEN = Item('a', 'b')
    BLUE = Item('c', 'd')

These days, any supported version of Python has enum, so please use that module. It gives you a lot more control over how each enum value is produced.

If you give each item a tuple of values, then these are passed to the __init__ method as separate (positional) arguments, which lets you set additional attributes on the enum value:

from enum import Enum

class Items(Enum):
    GREEN = ('a', 'b')
    BLUE = ('c', 'd')

    def __init__(self, a, b):
        self.a = a
        self.b = b

This produces enum entries whose value is the tuple assigned to each name, as well as two attributes a and b:

>>> Items.GREEN, Items.BLUE
(<Items.GREEN: ('a', 'b')>, <Items.BLUE: ('c', 'd')>)
>>> Items.BLUE.a
'c'
>>> Items.BLUE.b
'd'
>>> Items(('a', 'b'))
<Items.GREEN: ('a', 'b')>

Note that you can look up each enum value by passing in the same tuple again.

If the first item should represent the value of each enum entry, use a __new__ method to set _value_:

from enum import Enum

class Items(Enum):
    GREEN = ('a', 'b')
    BLUE = ('c', 'd')

    def __new__(cls, a, b):
        entry = object.__new__(cls) 
        entry.a = entry._value_ = a  # set the value, and the extra attribute
        entry.b = b
        return entry

    def __repr__(self):
        return f'<{type(self).__name__}.{self.name}: ({self.a!r}, {self.b!r})>'

I added a custom __repr__ as well, the default only includes self._value_. Now the value of each entry is defined by the first item in the tuple, and can be used to look up the enum entry:

>>> Items.GREEN, Items.BLUE
(<Items.GREEN: ('a', 'b')>, <Items.BLUE: ('c', 'd')>)
>>> Items.BLUE.a
'c'
>>> Items.BLUE.b
'd'
>>> Items('a')
<Items.GREEN: ('a', 'b')>

See the section on __init__ vs. __new__ in the documentation for further options.

ted
  • 4,791
  • 5
  • 38
  • 84
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • @bmargulies Or some watching. In this video recording of a PyCon talk] Raymond Hettinger explains why named tuples are awesome, how they work and use cases for them. [Fun with Python's Newer Tools](http://pyvideo.org/video/367/pycon-2011--fun-with-python--39-s-newer-tools) `[11:35 - 26:00]`. The examples he demonstrates include an enum type for colors. – Lukas Graf Oct 01 '12 at 20:44
  • Doesn't allow me to chose an item from its name e.g. I expect `Items('a')` to return `Items.GREEN` – off99555 Oct 16 '20 at 08:54
  • @off99555: this answer was posted long before the `enum` module was added, which is what you really want to use now. – Martijn Pieters Oct 16 '20 at 09:10
  • 2
    @off99555: I've updated the answer to make use of `enum` now that that's a universally available option. Note that an `Enum` won't directly let you map arbitrary items from the tuple of options to an enum value; `Items('b')` or `Items('d')` still won't work, only the enum `_value_` attribute is supported in lookups. You'd have to define a class method yourself that encodes custom lookup rules, e.g. `@classmethod`, `def lookup(cls, value):`, `for entry in cls.__members__.values():` `if value in {entry.a, entry.b}: return entry`. – Martijn Pieters Oct 16 '20 at 09:21
  • There is a `def _missing_(cls, value):` method to call lookup automatically (py 3.6+) – Александр Sep 01 '23 at 10:26
32

Here's another approach which I think is simpler than the others, but allows the most flexibility:

from collections import namedtuple
from enum import Enum

class Status(namedtuple('Status', 'name description'), Enum):
    READY = 'ready', 'I am ready to do whatever is needed'
    ERROR = 'error', 'Something went wrong here'

    def __str__(self) -> str:
        return self.name

It works as expected:

>>> str(Status.READY)
ready

>>> Status.READY
<Status.READY: Status(name='ready', description='I am ready to do whatever is needed')>

>>> Status.READY.description
'I am ready to do whatever is needed'

>>> Status.READY.value
Status(name='ready', description='I am ready to do whatever is needed')

Also you are able to retrieve the enum by name (Thanks @leoll2 for pointing this out). For example

>>> Status['READY']
<Status.READY: Status(name='ready', description='I am ready to do whatever is needed')>

You get the best of namedtuple and Enum.

leafmeal
  • 1,824
  • 15
  • 15
  • 2
    This is more descriptive and easy to use than the accepted answer. – Nairum Aug 25 '20 at 16:05
  • 4
    It doesn't allow me to select the Status by name e.g. `Status('ready')`. It's needed when you want user to choose the status from a list of names, and they will type a name e.g. `'ready'` – off99555 Oct 16 '20 at 08:53
  • 3
    @off99555 You can do it with `Status["READY"]` – leoll2 Mar 18 '22 at 10:57
8

After searching a lot, I found these two working examples!

That's it my friends!

Codes...

from enum import Enum


class StatusInt(int, Enum):
    READY = (0, "Ready to go!")
    ERROR = (1, "Something wrong!")

    def __new__(cls, value, description):
        obj = int.__new__(cls, value)
        obj._value_ = value
        obj._description_ = description
        return obj

    @property
    def description(self):
        return self._description_


class StatusObj(Enum):
    READY = (0, "Ready to go!")
    ERROR = (1, "Something wrong!")

    def __init__(self, value, description):
        self._value_ = value
        self._description_ = description

    @property
    def description(self):
        return self._description_


print(str(StatusInt.READY == StatusInt.ERROR))
print(str(StatusInt.READY.value))
print(StatusInt.READY.description)

print(str(StatusObj.READY == StatusObj.ERROR))
print(str(StatusObj.READY.value))
print(StatusObj.READY.description)

Outputs...

False
0
Ready to go!
False
0
Ready to go!

[Ref(s).: https://docs.python.org/3/library/enum.html#when-to-use-new-vs-init , https://docs.python.org/3/library/enum.html#planet ]

Eduardo Lucio
  • 1,771
  • 2
  • 25
  • 43
3

for small enums @property might work:

class WikiCfpEntry(Enum):
    '''
    possible supported storage modes
    '''
    EVENT = "Event"      
    SERIES = "Series"
    
    @property
    def urlPrefix(self):
        baseUrl="http://www.wikicfp.com/cfp"
        if self==WikiCfpEntry.EVENT:
            url= f"{baseUrl}/servlet/event.showcfp?eventid="
        elif self==WikiCfpEntry.SERIES:
            url= f"{baseUrl}/program?id="
        return url
Wolfgang Fahl
  • 15,016
  • 11
  • 93
  • 186
2

For keyword-based initialization of attributes, you might try data-enum, a more lightweight implementation of enum with cleaner syntax for some cases, including this one.

from data_enum import DataEnum

class Item(DataEnum):
    data_attribute_names = ('a', 'b')

Item.GREEN = Item(a='a', b='b')
Item.BLUE = Item(a='c', b='d')

I should note that I am the author of data-enum, and built it specifically to address this use case.

Chase Finch
  • 5,161
  • 1
  • 21
  • 20
1

Inspired by some of the other answers, I found a way of including additional fields to an enum as 'transparently' as possible, overcoming some shortcomings of the other approaches. Everything works the same as if the additional fields weren't there.

The enum is immutable just like a tuple, the value of the enum is just as it would be without the additional fields, it works just like a normal enum with auto(), and selecting an enum by value works.

import enum

# Common base class for all enums you want to create with additional fields (you only need this once)
class EnumFI(enum.Enum):

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls._values = []

    def __new__(cls, *args, **kwargs):
        value = args[0]
        if isinstance(value, enum.auto):
            if value.value == enum._auto_null:
                value.value = cls._generate_next_value_(None, 1, len(cls.__members__), cls._values[:])  # Note: This just passes None for the key, which is generally okay
            value = value.value
            args = (value,) + args[1:]
        cls._values.append(value)
        instance = cls._member_type_.__new__(cls, *args, **kwargs)
        instance._value_ = value
        return instance

    def __format__(self, format_spec):
        return str.__format__(str(self), format_spec)

Then anywhere in the code you can just do:

from enum import auto
from collections import namedtuple

class Color(namedtuple('ColorTuple', 'id r g b'), EnumFI):
    GREEN = auto(), 0, 255, 0
    BLUE = auto(), 0, 0, 255

Example output:

In[4]: Color.GREEN
Out[4]: <Color.GREEN: 1>

In[5]: Color.GREEN.value
Out[5]: 1

In[6]: Color.GREEN.r
Out[6]: 0

In[7]: Color.GREEN.g
Out[7]: 255

In[8]: Color.GREEN.b
Out[8]: 0

In[9]: Color.GREEN.r = 8
Traceback (most recent call last):
  File "/home/phil/anaconda3/envs/dl/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 3326, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-9-914a059d9d3b>", line 1, in <module>
    Color.GREEN.r = 8
AttributeError: can't set attribute

In[10]: Color(2)
Out[10]: <Color.BLUE: 2>

In[11]: Color['BLUE']
Out[11]: <Color.BLUE: 2>
pallgeuer
  • 1,216
  • 1
  • 7
  • 17
0

enum-properties provides an extension of the Enum base class that allows attributes on enum values and also allows symmetric mapping backwards from attribute values to their enumeration values.

Add properties to Python enumeration values with a simple declarative syntax. Enum Properties is a lightweight extension to Python's Enum class. Example:

from enum_properties import EnumProperties, p
from enum import auto

class Color(EnumProperties, p('rgb'), p('hex')):

    # name   value      rgb       hex
    RED    = auto(), (1, 0, 0), 'ff0000'
    GREEN  = auto(), (0, 1, 0), '00ff00'
    BLUE   = auto(), (0, 0, 1), '0000ff'

# the named p() values in the Enum's inheritance become properties on
# each value, matching the order in which they are specified

Color.RED.rgb   == (1, 0, 0)
Color.GREEN.rgb == (0, 1, 0)
Color.BLUE.rgb  == (0, 0, 1)

Color.RED.hex   == 'ff0000'
Color.GREEN.hex == '00ff00'
Color.BLUE.hex  == '0000ff'

Properties may also be symmetrically mapped to enumeration values, using s() values:

from enum_properties import EnumProperties, s
from enum import auto

class Color(EnumProperties, s('rgb'), s('hex', case_fold=True)):

    RED    = auto(), (1, 0, 0), 'ff0000'
    GREEN  = auto(), (0, 1, 0), '00ff00'
    BLUE   = auto(), (0, 0, 1), '0000ff'

# any named s() values in the Enum's inheritance become properties on
# each value, and the enumeration value may be instantiated from the
# property's value

Color((1, 0, 0)) == Color.RED
Color((0, 1, 0)) == Color.GREEN
Color((0, 0, 1)) == Color.BLUE

Color('ff0000') == Color.RED
Color('FF0000') == Color.RED  # case_fold makes mapping case insensitive
Color('00ff00') == Color.GREEN
Color('00FF00') == Color.GREEN
Color('0000ff') == Color.BLUE
Color('0000FF') == Color.BLUE

Color.RED.hex == 'ff0000'
bckohan
  • 203
  • 1
  • 6