158

Is it possible to extend classes created using the new Enum functionality in Python 3.4? How?

Simple subclassing doesn't appear to work. An example like

from enum import Enum

class EventStatus(Enum):
   success = 0
   failure = 1

class BookingStatus(EventStatus):
   duplicate = 2
   unknown = 3

will give an exception like TypeError: Cannot extend enumerations or (in more recent versions) TypeError: BookingStatus: cannot extend enumeration 'EventStatus'.

How can I make it so that BookingStatus reuses the enumeration values from EventStatus and adds more?

6
  • 22
    The idea behind an enum is that you have a complete list of all values of that type. If you extend it and add more values, you break the most fundamental property of enums. Commented Nov 12, 2015 at 19:59
  • @user2357112: thx, this is an answer for my problem. Commented Nov 12, 2015 at 20:07
  • 18
    @user2357112 I don't understand this — the above code doesn't add more values to EventStatus, it seeks to make a new type which inherits EventStatus's values and also has a few more. Afaics, EventStatus is unsullied. Why does this break a fundamental property? Commented Apr 16, 2019 at 10:42
  • 3
    @Cai: If this were allowed, the new values would be instances of EventStatus, because the new class would be a subclass of EventStatus. (This is the same reason you can't subclass bool.) Commented Apr 16, 2019 at 17:12
  • 4
    @sleepystar96: You're misunderstanding something about enums and the isinstance relationship. One of the core properties of an enum class is that you are not allowed to make more instances. Instances of a subclass would be more instances. The fact that method overrides in one subclass wouldn't show up on other subclasses is completely irrelevant. Commented Feb 3, 2022 at 21:21

17 Answers 17

101

Subclassing an enumeration is allowed only if the enumeration does not define any members.

Allowing subclassing of enums that define members would lead to a violation of some important invariants of types and instances.

https://docs.python.org/3/howto/enum.html#restricted-enum-subclassing

So no, it's not directly possible.

Sign up to request clarification or add additional context in comments.

9 Comments

Is there any other built-in method for do this?
@falek.marcin: Probably not. I think that the best thing you can do is to use another, simpler implementation of Enum. There are plenty of them.
For the record, the reason it makes no sense is that Enum types use identity based comparisons and the values of the enumeration are immutable. And the type of a value is important when determining relative ordering, membership testing, etc. When you define success, its type is immutably set as EventStatus; if you were allowed to inherit from EventStatus, suddenly success would need to be BookingStatus too (doing so would violate immutability and/or cause issues with the type based testing).
It seems that the link to the section in the enum docs has changed. I cannot edit the answer myself because the suggested edit queue is full.
Note the "important invariants os types" is the Liskov substitution principle (en.wikipedia.org/wiki/Liskov_substitution_principle). To understand, consider def func(val: BaseEnum). And now call with func(DerivedEnum.some-value-not-in-base)). What should happen here, considereing that python BaseEnum do not allow undefined values? So subclassing enums is incompatible with proper object oriented deisgn
|
72

While uncommon, it is sometimes useful to create an enum from many modules. The aenum1 library supports this with an extend_enum function:

from aenum import Enum, extend_enum

class Index(Enum):
    DeviceType    = 0x1000
    ErrorRegister = 0x1001

for name, value in (
        ('ControlWord', 0x6040),
        ('StatusWord', 0x6041),
        ('OperationMode', 0x6060),
        ):
    extend_enum(Index, name, value)

assert len(Index) == 5
assert list(Index) == [Index.DeviceType, Index.ErrorRegister, Index.ControlWord, Index.StatusWord, Index.OperationMode]
assert Index.DeviceType.value == 0x1000
assert Index.StatusWord.value == 0x6041

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

1 Comment

don't suppose 'extend_Strenum' is on the road map?
34

Calling the Enum class directly and making use of chain allows the extension (joining) of an existing enum.

I came upon the problem of extending enums while working on a CANopen implementation. Parameter indices in the range from 0x1000 to 0x2000 are generic to all CANopen nodes while e.g. the range from 0x6000 onwards depends open whether the node is a drive, io-module, etc.

nodes.py:

from enum import IntEnum

class IndexGeneric(IntEnum):
    """ This enum holds the index value of genric object entrys
    """
    DeviceType    = 0x1000
    ErrorRegister = 0x1001

Idx = IndexGeneric
 

drives.py:

from itertools import chain
from enum import IntEnum
from nodes import IndexGeneric

class IndexDrives(IntEnum):
    """ This enum holds the index value of drive object entrys
    """
    ControlWord   = 0x6040
    StatusWord    = 0x6041
    OperationMode = 0x6060

Idx= IntEnum('Idx', [(i.name, i.value) for i in chain(IndexGeneric,IndexDrives)])

2 Comments

I would like to use code derived from your final snippet in the MIT-licensed typhon library. Are you willing to relicense this under a MIT-compatible license? I will keep full attribution.
@gerrit yes, sure feel free to use it! sorry for the late reply
24

I tested that way on 3.8. We may inherit existing enum but we need to do it also from base class (at last position).

Docs:

A new Enum class must have one base Enum class, up to one concrete data type, and as many object-based mixin classes as needed. The order of these base classes is:

class EnumName([mix-in, ...,] [data-type,] base-enum):
    pass

Example:

class Cats(Enum):
    SIBERIAN = "siberian"
    SPHINX = "sphinx"


class Animals(Cats, Enum):
    LABRADOR = "labrador"
    CORGI = "corgi"

After that you may access Cats from Animals:

>>> Animals.SIBERIAN
<Cats.SIBERIAN: 'siberian'>

But if you want to iterate over this enum, only new members were accessible:

>>> list(Animals)
[<Animals.LABRADOR: 'labrador'>, <Animals.CORGI: 'corgi'>]

Actually this way is for inheriting methods from base class, but you may use it for members with these restrictions.

Another way (a bit hacky)

As described above, to write some function to join two enums in one. I've wrote that example:

def extend_enum(inherited_enum):
    def wrapper(added_enum):
        joined = {}
        for item in inherited_enum:
            joined[item.name] = item.value
        for item in added_enum:
            joined[item.name] = item.value
        return Enum(added_enum.__name__, joined)
    return wrapper


class Cats(Enum):
    SIBERIAN = "siberian"
    SPHINX = "sphinx"


@extend_enum(Cats)
class Animals(Enum):
    LABRADOR = "labrador"
    CORGI = "corgi"

But here we meet another problems. If we want to compare members it fails:

>>> Animals.SIBERIAN == Cats.SIBERIAN
False

Here we may compare only names and values of newly created members:

>>> Animals.SIBERIAN.value == Cats.SIBERIAN.value
True

But if we need iteration over new Enum, it works ok:

>>> list(Animals)
[<Animals.SIBERIAN: 'siberian'>, <Animals.SPHINX: 'sphinx'>, <Animals.LABRADOR: 'labrador'>, <Animals.CORGI: 'corgi'>]

So choose your way: simple inheritance, inheritance emulation with decorator (recreation in fact), or adding a new dependency like aenum (I haven't tested it, but I expect it support all features I described).

8 Comments

Best so far if you don't want to compare to base class values. Thanks
Python 3.8.6 broke this. Was working on python 3.8.5
Yeah, there is a bug in enum inheritance. I have encountered it on 3.8.6, but it hasn't reproduced in 3.8.2 and 3.9.0.
@MikhaliBulygin you are wrong. There was a bug in previous releases which they knowingly fixed in 3.8.6. See the link I posted
Ok, I guess this is a workaround that is not supposed to work. This is the issue that "fixed/broke" this from 8.3.6 onwards: bugs.python.org/issue41517
|
20

For correct type specification, you could use the Union operator:

from enum import Enum
from typing import Union

class EventStatus(Enum):
   success = 0
   failure = 1

class BookingSpecificStatus(Enum):
   duplicate = 2
   unknown = 3

BookingStatus = Union[EventStatus, BookingSpecificStatus]

example_status: BookingStatus
example_status = BookingSpecificStatus.duplicate
example_status = EventStatus.success

5 Comments

Worth Noting: The typing module requires Python 3.5+.
this doesn't seem to give any functionality though
Type annotation are only metadata that can be used by third-party static checking tools. A Union annotation doesn't actually create a new type; it merely documents that a particular name is intended to be used only for an instance of either of the two, still unrelated classes. In particular, this does not allow us to write e.g. BookingStatus.success or BookingStatus.duplicate.
This is actually the correct way to "solve" the initial problem. Extending enums violates Liskov substitution principle (en.wikipedia.org/wiki/Liskov_substitution_principle) one fundamental principle of object oriented programming. So extending Enums (similar to inheritance for classes) is a sign of bad software design. Union is indeed the right semantic mapping here. Even if this is not what people had perhaps in mind, one should not extend enums similar to classes for good reason.
For anyone seeking more information on why you can’t subclass enums, see this email from Guido (and the links it mentions).
15

Plenty of good answers here already but here's another one purely using Enum's Functional API.

Probably not the most beautiful solution but it avoids code duplication, works out of the box, no additional packages/libraries are need, and it should be sufficient to cover most use cases:

from enum import Enum

class EventStatus(Enum):
   success = 0
   failure = 1

BookingStatus = Enum(
    "BookingStatus",
    [es.name for es in EventStatus] + ["duplicate", "unknown"],
    start=0,
)

for bs in BookingStatus:
    print(bs.name, bs.value)

# success 0
# failure 1
# duplicate 2
# unknown 3

If you'd like to be explicit about the values assigned, you can use:

BookingStatus = Enum(
    "BookingStatus",
    [(es.name, es.value) for es in EventStatus] + [("duplicate", 6), ("unknown", 7)],
)

for bs in BookingStatus:
    print(bs.name, bs.value)

# success 0
# failure 1
# duplicate 6
# unknown 7

4 Comments

Instead of start=0 and list, you can just use a dict: {attr.name: attr.value for attr in EventStatus} | {"duplicate": 555, "unknown": -2}, (| only python > 3.9, for python < 3.9 use dict.update method or dict(**d1, **d2).
Good up to a point. Unfortunately EventStatus.success and BookingStatus.success are two different objects so do not compare equal. So that makes it unsuitable if passing an EventStatus to a method that expects a BookingStatus.
So - if you want to add methods to BookingStatus how do you do that? I wanted to use this method - but add helper methods..
@JGFMK - Not exactly pretty but you could do something like: BookingStatus.__class__.my_method = my_method, where my_method is defined by you and takes self as the first argument def my_method(self, arg_a, arg_b, ...): .... And then BookingStatus.my_method(arg_a, arg_b)
13

I've opted to use a metaclass approach to this problem.

from enum import EnumMeta

class MetaClsEnumJoin(EnumMeta):
    """
    Metaclass that creates a new `enum.Enum` from multiple existing Enums.

    @code
        from enum import Enum

        ENUMA = Enum('ENUMA', {'a': 1, 'b': 2})
        ENUMB = Enum('ENUMB', {'c': 3, 'd': 4})
        class ENUMJOINED(Enum, metaclass=MetaClsEnumJoin, enums=(ENUMA, ENUMB)):
            pass

        print(ENUMJOINED.a)
        print(ENUMJOINED.b)
        print(ENUMJOINED.c)
        print(ENUMJOINED.d)
    @endcode
    """

    @classmethod
    def __prepare__(metacls, name, bases, enums=None, **kargs):
        """
        Generates the class's namespace.
        @param enums Iterable of `enum.Enum` classes to include in the new class.  Conflicts will
            be resolved by overriding existing values defined by Enums earlier in the iterable with
            values defined by Enums later in the iterable.
        """
        #kargs = {"myArg1": 1, "myArg2": 2}
        if enums is None:
            raise ValueError('Class keyword argument `enums` must be defined to use this metaclass.')
        ret = super().__prepare__(name, bases, **kargs)
        for enm in enums:
            for item in enm:
                ret[item.name] = item.value  #Throws `TypeError` if conflict.
        return ret

    def __new__(metacls, name, bases, namespace, **kargs):
        return super().__new__(metacls, name, bases, namespace)
        #DO NOT send "**kargs" to "type.__new__".  It won't catch them and
        #you'll get a "TypeError: type() takes 1 or 3 arguments" exception.

    def __init__(cls, name, bases, namespace, **kargs):
        super().__init__(name, bases, namespace)
        #DO NOT send "**kargs" to "type.__init__" in Python 3.5 and older.  You'll get a
        #"TypeError: type.__init__() takes no keyword arguments" exception.

This metaclass can be used like so:

>>> from enum import Enum
>>>
>>> ENUMA = Enum('ENUMA', {'a': 1, 'b': 2})
>>> ENUMB = Enum('ENUMB', {'c': 3, 'd': 4})
>>> class ENUMJOINED(Enum, metaclass=MetaClsEnumJoin, enums=(ENUMA, ENUMB)):
...     e = 5
...     f = 6
...
>>> print(repr(ENUMJOINED.a))
<ENUMJOINED.a: 1>
>>> print(repr(ENUMJOINED.b))
<ENUMJOINED.b: 2>
>>> print(repr(ENUMJOINED.c))
<ENUMJOINED.c: 3>
>>> print(repr(ENUMJOINED.d))
<ENUMJOINED.d: 4>
>>> print(repr(ENUMJOINED.e))
<ENUMJOINED.e: 5>
>>> print(repr(ENUMJOINED.f))
<ENUMJOINED.f: 6>

This approach creates a new Enum using the same name-value pairs as the source Enums, but the resulting Enum members are still unique. The names and values will be the same, but they will fail direct comparisons to their origins following the spirit of Python's Enum class design:

>>> ENUMA.b.name == ENUMJOINED.b.name
True
>>> ENUMA.b.value == ENUMJOINED.b.value
True
>>> ENUMA.b == ENUMJOINED.b
False
>>> ENUMA.b is ENUMJOINED.b
False
>>>

Note what happens in the event of a namespace conflict:

>>> ENUMC = Enum('ENUMA', {'a': 1, 'b': 2})
>>> ENUMD = Enum('ENUMB', {'a': 3})
>>> class ENUMJOINEDCONFLICT(Enum, metaclass=MetaClsEnumJoin, enums=(ENUMC, ENUMD)):
...     pass
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 19, in __prepare__
  File "C:\Users\jcrwfrd\AppData\Local\Programs\Python\Python37\lib\enum.py", line 100, in __setitem__
    raise TypeError('Attempted to reuse key: %r' % key)
TypeError: Attempted to reuse key: 'a'
>>>

This is due to the base enum.EnumMeta.__prepare__ returning a special enum._EnumDict instead of the typical dict object that behaves different upon key assignment. You may wish to suppress this error message by surrounding it with a try-except TypeError, or there may be a way to modify the namespace before calling super().__prepare__(...).

Comments

7

If anyone is interested, here's my simple solution since all these answers seem pretty hacky anyway.

from enum import Enum

class EventStatus(Enum):
   success = 0
   failure = 1

class BookingStatus(Enum):
   duplicate = 2
   unknown = 3
# extend enum
BookingStatus._member_map_.update(EventStatus._member_map_)

Comments

5

Decorator to extend Enum

To expand on Mikhail Bulygin's answer, a decorator can be used to extend an Enum (and support equality by using a custom Enum base class).

1. Enum base class with value-based equality

from enum import Enum
from typing import Any


class EnumBase(Enum):
    def __eq__(self, other: Any) -> bool:
        if isinstance(other, Enum):
            return self.value == other.value
        return False

2. Decorator to extend Enum class

from typing import Callable

def extend_enum(parent_enum: EnumBase) -> Callable[[EnumBase], EnumBase]:
    """Decorator function that extends an enum class with values from another enum class."""
    def wrapper(extended_enum: EnumBase) -> EnumBase:
        joined = {}
        for item in parent_enum:
            joined[item.name] = item.value
        for item in extended_enum:
            joined[item.name] = item.value
        return EnumBase(extended_enum.__name__, joined)
    return wrapper

Example

>>> from enum import Enum
>>> from typing import Any, Callable
>>> class EnumBase(Enum):
        def __eq__(self, other: Any) -> bool:
            if isinstance(other, Enum):
                return self.value == other.value
            return False
>>> def extend_enum(parent_enum: EnumBase) -> Callable[[EnumBase], EnumBase]:
        def wrapper(extended_enum: EnumBase) -> EnumBase:
            joined = {}
            for item in parent_enum:
                joined[item.name] = item.value
            for item in extended_enum:
                joined[item.name] = item.value
            return EnumBase(extended_enum.__name__, joined)
        return wrapper
>>> class Parent(EnumBase):
        A = 1
        B = 2
>>> @extend_enum(Parent)
    class ExtendedEnum(EnumBase):
        C = 3
>>> Parent.A == ExtendedEnum.A
True
>>> list(ExtendedEnum)
[<ExtendedEnum.A: 1>, <ExtendedEnum.B: 2>, <ExtendedEnum.C: 3>]

Comments

3

Another way :

Letter = Enum(value="Letter", names={"A": 0, "B": 1})
LetterExtended = Enum(value="Letter", names=dict({"C": 2, "D": 3}, **{i.name: i.value for i in Letter}))

Or :

LetterDict = {"A": 0, "B": 1}
Letter = Enum(value="Letter", names=LetterDict)

LetterExtendedDict = dict({"C": 2, "D": 3}, **LetterDict)
LetterExtended = Enum(value="Letter", names=LetterExtendedDict)

Output :

>>> Letter.A
<Letter.A: 0>
>>> Letter.C
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "D:\jhpx\AppData\Local\Programs\Python\Python36\lib\enum.py", line 324, in __getattr__
    raise AttributeError(name) from None
AttributeError: C
>>> LetterExtended.A
<Letter.A: 0>
>>> LetterExtended.C
<Letter.C: 2>

Comments

3

I think you could do it in this way:

from typing import List
from enum import Enum

def extend_enum(current_enum, names: List[str], values: List = None):
    if not values:
        values = names

    for item in current_enum:
        names.append(item.name)
        values.append(item.value)

    return Enum(current_enum.__name__, dict(zip(names, values)))

class EventStatus(Enum):
   success = 0
   failure = 1

class BookingStatus(object):
   duplicate = 2
   unknown = 3

BookingStatus = extend_enum(EventStatus, ['duplicate','unknown'],[2,3])

the key points is:

  • python could change anything at runtime
  • class is object too

Comments

3

Conceptually, it does not make sense to extend an enumeration in this sense. The problem is that this violates the Liskov Substitution Principle: instances of a subclass are supposed to be usable anywhere an instance of the base class could be used, but an instance of BookingStatus could not reliably be used anywhere that an EventStatus is expected. After all, if that instance had a value of BookingStatus.duplicate or BookingStatus.unknown, that would not be a valid enumeration value for an EventStatus.

We can create a new class that reuses the EventStatus setup by using the functional API. A basic example:

event_status_codes = [s.name for s in EventStatus]
BookingStatus = Enum(
    'BookingStatus', event_status_codes + ['duplicate', 'unknown']
)

This approach re-numbers the enumeration values, ignoring what they were in EventStatus. We can also pass name-value pairs in order to specify the enum values; this lets us do a bit more analysis, in order to reuse the old values and auto-number new ones:

def extend_enum(result_name, base, *new_names):
    base_values = [(v.name, v.value) for v in base]
    next_number = max(v.value for v in base) + 1
    new_values = [(name, i) for i, name in enumerate(new_names, next_number)]
    return Enum(result_name, base_values + new_values)

# Now we can do:
BookingStatus = extend_enum('BookingStatus', EventStatus, 'duplicate', 'unknown')

Comments

2

You can't extend enums but you can create a new one by merging them.
Tested for Python 3.6

from enum import Enum


class DummyEnum(Enum):
    a = 1


class AnotherDummyEnum(Enum):
    b = 2


def merge_enums(class_name: str, enum1, enum2, result_type=Enum):
    if not (issubclass(enum1, Enum) and issubclass(enum2, Enum)):
        raise TypeError(
            f'{enum1} and {enum2} must be derived from Enum class'
        )

    attrs = {attr.name: attr.value for attr in set(chain(enum1, enum2))}
    return result_type(class_name, attrs, module=__name__)


result_enum = merge_enums(
    class_name='DummyResultEnum',
    enum1=DummyEnum,
    enum2=AnotherDummyEnum,
)

Comments

1

In case someone finds themselves here like me, looking for a solution to giving some Enums some common methods, but are hit with the above error message, you can use mixins like this as long as the mixin defines no attributes.

For example a mixin that defines a convenient .values() method on an Enum that returns all the enum's option values (e.g. to solve something like this generically):

class GetValuesEnumMixin()

    @classmethod
    def values(self):
        return [attr.value for attr in self]

which you can use with Enums you define:

class SomeEnum(GetValuesEnumMixin, Enum):
    FIRST = "Primero"
    SECOND = "Secundo"

And in your code you could use the extended Enum like so:

SomeEnum.values()
>>> ["Primero", "Secundo"]

2 Comments

thanks for this. I was also able to do the same with GetValuesEnumMixin(Enum) and SomeEnum(GetValuesEnumMixin). FYI: SomeEnum.values didn't work for me but SomeEnum.values() did.
Ah yes, of course. Wrote the answer from memory, so swapped property and method. Updated above.
1

I suggest a concise way

from enum import Enum


def extend_enum(*enums, enum_name="EnumName"):
    return Enum(enum_name, {i.name: i.value for e in enums for i in e})


AllStatuses = extend_enum(EventStatus, BookingStatus)

1 Comment

I would also like the IDE to prompt me with the available options for the new Enum: AllStatuses: Union[EventStatus, BookingStatus] = extend_enum(EventStatus, BookingStatus)
0

Yes, you can modify an Enum. The example code, below, is somewhat hacky and it obviously depends on internals of Enum which it has no business whatsoever to depend on. On the other hand, it works.

class ExtIntEnum(IntEnum):
    @classmethod
    def _add(cls, value, name):
        obj = int.__new__(cls, value)
        obj._value_ = value
        obj._name_ = name  
        obj.__objclass__ = cls

        cls._member_map_[name] = obj
        cls._value2member_map_[value] = obj
        cls._member_names_.append(name)    

class Fubar(ExtIntEnum):
    foo = 1
    bar = 2

Fubar._add(3,"baz")
Fubar._add(4,"quux")

Specifically, observe the obj = int.__new__() line. The enum module jumps through a few hoops to find the correct __new__ method for the class that should be enumerated. We ignore these hoops here because we already know how integers (or rather, instances of subclasses of int) are created.

It's a good idea not to use this in production code. If you have to, you really should add guards against duplicate values or names.

Comments

0

I wanted to inherit from Django's IntegerChoices which is not possible due to the "Cannot extend enumerations" limitation. I figured it could be done by a relative simple metaclass.

CustomMetaEnum.py:

class CustomMetaEnum(type):
    def __new__(self, name, bases, namespace):
        # Create empty dict to hold constants (ex. A = 1)
        fields = {}

        # Copy constants from the namespace to the fields dict.
        fields = {key:value for key, value in namespace.items() if isinstance(value, int)}
    
        # In case we're about to create a subclass, copy all constants from the base classes' _fields.
        for base in bases:
            fields.update(base._fields)

        # Save constants as _fields in the new class' namespace.
        namespace['_fields'] = fields
        return super().__new__(self, name, bases, namespace)

    # The choices property is often used in Django.
    # If other methods such as values(), labels() etc. are needed
    # they can be implemented below (for inspiration [Django IntegerChoice source][1])
    @property
    def choices(self):
        return [(value,key) for key,value in self._fields.items()]

main.py:

from CustomMetaEnum import CustomMetaEnum

class States(metaclass=CustomMetaEnum):
    A = 1
    B = 2
    C = 3

print("States: ")
print(States.A)
print(States.B)
print(States.C)
print(States.choices)


print("MoreStates: ")
class MoreStates(States):
    D = 22
    pass

print(MoreStates.A)
print(MoreStates.B)
print(MoreStates.C)
print(MoreStates.D)
print(MoreStates.choices)

python3.8 main.py:

States: 
1
2
3
[(1, 'A'), (2, 'B'), (3, 'C')]
MoreStates: 
1
2
3
22
[(22, 'D'), (1, 'A'), (2, 'B'), (3, 'C')]

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.