Enum of strings

Viewed 195

In my app, I am passing error reasons in my JSON API like this: {"ok": false, "reason": "EMAIL_ALREADY_REGISTERED"}. However, using plain strings like this is very vulnerable to errors like typing just "EMAIL_REGISTERED", or various typos.

So I thought about creating some util to only allow fixed set of values. My first idea was Enum:

from enum import Enum
class ErrorReason(Enum):
    EXCEPTION = 1
    EMAIL_ALREADY_REGISTERED = 2
    PASSWORD_TOO_SHORT = 3

This is great that IDE (PyCharm) automatically suggests me possible values when I type ErrorReason.E and checks if the given value is valid, however, it has drawbacks too:

  • there are unnecessary numeric values that I don't ever need
  • serializing this value when passing it around: str creates "ErrorReason.EXCEPTION", but I can also do ErrorReason.EXCEPTION.name, or .value (getting the numeric value), and flask.jsonify doesn't support it by default, so I need to set a JSON serializer subclass
  • it also feels to me that this isn't a correct/intended use of Enum

One other way to do this could be this:

class ErrorReason:
    EXCEPTION = "EXCEPTION"
    EMAIL_ALREADY_REGISTERED = "EMAIL_ALREADY_REGISTERED"
    PASSWORD_TOO_SHORT = "PASSWORD_TOO_SHORT"

This looks a bit cleaner, and ErrorReason.EXCEPTION evaluates to simple string, but it feels wrong too – I have to write every possible value twice, and object with this sole purpose feels overkill to me.

What is the best way to achieve this? Or at least, what's the best way to create the "dumb" simple object in the last example without typing everything twice, while keeping smart IDE suggestions?


Edit 1: I found a way to generate the given class. However, even though I generate annotations, PyCharm still doesn't do any autocomplete suggesting.

_attrs = {"__annotations__": {}}
for reason in ("EXCEPTION", "PASSWORD_TOO_SHORT", "EMAIL_ALREADY_REGISTERED"):
    _attrs[reason] = reason
    _attrs["__annotations__"][reason] = str

ErrorReason = type("ErrorReason", (), _attrs)

See beginning of this answer for how are classes dynamically created.

2 Answers

You could use automatic enum values and change the behavior of auto() and __str__:

from enum import Enum, auto

class ErrorReason(str, Enum):

    def _generate_next_value_(name, start, count, last_values):
        return name

    def __str__(self):
        return self.name

    EXCEPTION = auto()
    EMAIL_ALREADY_REGISTERED = auto()
    PASSWORD_TOO_SHORT = auto()

Now: print(ErrorReason.EMAIL_ALREADY_REGISTERED) will be just EMAIL_ALREADY_REGISTERED

As stated in the answer by Tom Wojcik, also inherit from str, to make it serializable by json.

there are unnecessary numeric values that I don't ever need

You need them. It's literally what you return. I know what you mean by having a need for only one of name or value, but it's better to keep them the same. auto helps with that.

serializing this value when passing it around: str creates "ErrorReason.EXCEPTION", but I can also do ErrorReason.EXCEPTION.name, or .value (getting the numeric value), and flask.jsonify doesn't support it by default, so I need to set a JSON serializer subclass

Indeed, Enum is not serializable.

By default this will fail

from enum import Enum
import json


class ErrorReason(Enum):
    EXCEPTION = "EXCEPTION"
    EMAIL_ALREADY_REGISTERED = "EMAIL_ALREADY_REGISTERED"
    PASSWORD_TOO_SHORT = "PASSWORD_TOO_SHORT"


print(json.dumps({"exc": ErrorReason.EXCEPTION}))

with

TypeError: Object of type ErrorReason is not JSON serializable

That's why it's encouraged to use it with str (look at parents).

from enum import Enum
import json


class ErrorReason(str, Enum):
    EXCEPTION = "EXCEPTION"
    EMAIL_ALREADY_REGISTERED = "EMAIL_ALREADY_REGISTERED"
    PASSWORD_TOO_SHORT = "PASSWORD_TOO_SHORT"


print(json.dumps({"exc": ErrorReason.EXCEPTION}))

Will serialize just fine with str mixin.

Related