Mike Slinn
Mike Slinn

Fun With Python Enums

Published 2022-02-10. Last modified 2022-03-10.
Time to read: 3 minutes.

This site is categorized under Python.

This blog post demonstrates how to define additional properties for Python 3 enums. Defining an additional property in a Python enum can provide a simple way to provide string values. The concept is then expanded to demonstrate composition, an important concept for functional programming. This post concludes with a demonstration of dynamic dispatch in Python, by further extending an enum.

Adding Properties to Python Enums

Searching for python enum string value yields some complex and arcane ways to approach the problem.

Below is a short example of a Python enum that demonstrates a simple way to provide lower-case string values for enum constants: a new property, to_s, is defined. This property provides the string representation that is required. You could define other properties and methods to suit the needs of other projects.

cad_enums.py
"""Defines enums"""
from enum import Enum, auto
class EntityType(Enum): """Types of entities""" SITE = auto() GROUP = auto() COURSE = auto() SECTION = auto() LECTURE = auto()
@property def to_s(self) -> str: """:return: lower-case name of this instance""" return self.name.lower()

Adding the following to the bottom of the program allows us to demonstrate it:

cad_enums.py (part 2)
if __name__ == "__main__":  # Just for demonstration
    print("Specifying individual values:")
    print(f"  {EntityType.SITE.value}: {EntityType.SITE.to_s}")
    print(f"  {EntityType.GROUP.value}: {EntityType.GROUP.to_s}")
    print(f"  {EntityType.COURSE.value}: {EntityType.COURSE.to_s}")
    print(f"  {EntityType.SECTION.value}: {EntityType.SECTION.to_s}")
    print(f"  {EntityType.LECTURE.value}: {EntityType.LECTURE.to_s}")

    print("\nIterating through all values:")
    for entity_type in EntityType:
        print(f"  {entity_type.value}: {entity_type.to_s}")

Running the program produces this output:

Shell
$ cad_enums.py
Specifying individual values:
  1: site
  2: group
  3: course
  4: section
  5: lecture

Iterating through all values:
  1: site
  2: group
  3: course
  4: section
  5: lecture 
😁

Easy!

Constructing Enums

Enum constructors work the same as other Python class constructors. There are several ways to make a new instance of a Python enum. Let's try two ways by using the Python interpreter. Throughout this blog post I've inserted a blank line between Python interpreter prompts for readability.

Python
$ python
Python 3.9.7 (default, Sep 10 2021, 14:59:43)
[GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.

>>> from cad_enums import EntityType

>>> # Specify the desired enum constant value symbolically
>>> gtype = EntityType.GROUP
>>> print(gtype)
EntityType.GROUP 

>>> # Specify the desired enum constant value numerically
>>> stype = EntityType(1)
>>> print(stype)
EntityType.SITE 

Enum Ordering

A program I am working on needs to obtain the parent EntityType. By 'parent' I mean the EntityType with the next lowest numeric value. For example, the parent of EntityType.GROUP is EntityType.SITE. We can obtain a parent enum by computing its numeric value by adding the following method to the EntityType class definition.

Python
@property
def parent(self) -> 'EntityType':
    """:return: entity type of parent; site has no parent"""
    return EntityType(max(self.value - 1, 1))

The return type above is enclosed in quotes ('EntityType') to keep Python's type checker happy, because this is a forward reference. This is a forward reference because the type is referenced before it is fully compiled.

The complete enum class definition is now:

cad_enums.py
"""Defines enums"""
from enum import Enum, auto
class EntityType(Enum): """Types of entities""" SITE = auto() GROUP = auto() COURSE = auto() SECTION = auto() LECTURE = auto()
@property def to_s(self) -> str: """:return: lower-case name of this instance""" return self.name.lower()
@property def parent(self) -> 'EntityType': """:return: entity type of parent; site has no parent""" return EntityType(max(self.value - 1, 1))

Lets try out the new parent property in the Python interpreter.

Python
>>> EntityType.LECTURE.parent
<EntityType.SECTION: 4> 

>>> EntityType.SECTION.parent
<EntityType.COURSE: 3> 

>>> EntityType.COURSE.parent
<EntityType.GROUP: 2> 

>>> EntityType.GROUP.parent
<EntityType.SITE: 1> 

>>> EntityType.SITE.parent
<EntityType.SITE: 1> 

Enum Composition

Like methods and properties in all other Python classes, enum methods and properties compose if they return an instance of the class. Composition is also known as method chaining, and also can apply to class properties. Composition is an essential practice of a functional programming style.

The parent property returns an instance of the EntityType enum class, so it can be composed with any other property or method of that class, for example the to_s property shown earlier.

Python
>>> EntityType.LECTURE.parent.to_s
'section' 

>>> EntityType.SECTION.parent.to_s
'course' 

>>> EntityType.COURSE.parent.to_s
'group' 

>>> EntityType.GROUP.parent.to_s
'site' 

>>> EntityType.SITE.parent.to_s
'site' 

Dynamic Dispatch

The Python documentation might lead someone to assume that writing dynamic dispatch code is more complex than it actually is.

To summarize the documentation, all Python classes, methods and instances are callable. Callable functions have type Callable[[InputArg1Type, InputArg2Type], ReturnType]. If you do not want any type checking, write Callable[..., Any]. However, this is not very helpful information for dynamic dispatch. Fortunately, working with Callable is very simple.

You can pass around any Python class, constructor, function or method, and later provide it with the usual arguments. Invocation just works.

Let me show you how easy it is to write dynamic dispatch code in Python, let's construct one of five classes, depending on the value of an enum. First, we need a class definition for each enum value:

Python
# pylint: disable=too-few-public-methods

class BaseClass():
    """Demo only"""

class TestLecture(BaseClass):
    """This constructor has type Callable[[int, str], TestLecture]"""
    def __init__(self, id_: int, action: str):
        print(f"CadLecture constructor called with id {id_} and action {action}")

class TestSection(BaseClass):
    """This constructor has type Callable[[int, str], TestSection]"""
    def __init__(self, id_: int, action: str):
        print(f"CadSection constructor called with id {id_} and action {action}")

class TestCourse(BaseClass):
    """This constructor has type Callable[[int, str], TestCourse]"""
    def __init__(self, id_: int, action: str):
        print(f"CadCourse constructor called with id {id_} and action {action}")

class TestGroup(BaseClass):
    """This constructor has type Callable[[int, str], TestGroup]"""
    def __init__(self, id_: int, action: str):
        print(f"CadGroup constructor called with id {id_} and action {action}")

class TestSite(BaseClass):
    """This constructor has type Callable[[int, str], TestSite]"""
    def __init__(self, id_: int, action: str):
        print(f"CadSite constructor called with id {id_} and action {action}")

Now lets add another method, called construct, to EntityType that invokes the appropriate constructor according to the value of an EntityType instance:

Python
@property
def construct(self) -> Callable:
    """:return: the appropriate Callable for each enum value"""
    if self == EntityType.LECTURE:
        return TestLecture
    if self == EntityType.SECTION:
        return TestSection
    if self == EntityType.COURSE:
        return TestCourse
    if self == EntityType.GROUP:
        return TestGroup
    return TestSite

Using named arguments makes your code resistant to problems that might sneak in due to parameters changing over time.

I favor using named arguments at all times; it avoids many problems. As code evolves, arguments might be added or removed, or even reordered.

Let's test out dynamic dispatch in the Python interpreter. A class specific to each EntityType value is constructed by invoking the appropriate Callable and passing it named arguments id_ and action.

Python
>>> EntityType.LECTURE.construct(id_=55, action="gimme_lecture")
TestLecture constructor called with id 55 and action gimme_lecture
<entity_types.TestLecture object at 0x7f9aac690070> 

>>> EntityType.SECTION.construct(id_=13, action="gimme_section")
TestSection constructor called with id 13 and action gimme_section
<entity_types.TestSection object at 0x7f9aac5c1730> 

>>> EntityType.COURSE.construct(id_=40, action="gimme_course")
TestCourse constructor called with id 40 and action gimme_course
<entity_types.TestCourse object at 0x7f9aac6900a0> 

>>> EntityType.GROUP.construct(id_=103, action="gimme_group")
TestGroup constructor called with id 103 and action gimme_group
<entity_types.TestGroup object at 0x7f9aac4c6b20> 

>>> EntityType.SITE.construct(id_=1, action="gimme_site")
TestSite constructor called with id 1 and action gimme_site
<entity_types.TestSite object at 0x7f9aac5c1730> 

Because these factory methods return the newly created instance of the desired type, the string representation is printed on the console after the method finishes outputting its processing results, for example: <entity_types.TestLecture object at 0x7f9aac690070>.

Using enums to construct class instances and/or invoke methods (aka dynamic dispatch) is super powerful. It rather resembles generics, actually, even though Python's support for generics is still in its infancy.

The complete Python program discussed in this post is here.