Back to Blog
Python

Working with Python Enum Values

python enum values: Learn how to define, access, iterate, and validate Python enum values, including practical patterns for using auto values, aliases, and custom data.

EnumPythonData ModelingType SafetyCode Quality
Python enum values concept illustrated with labeled containers and a mapping diagram

When you need a fixed set of named values, Python's enum module gives you more than a collection of constants. The Enum class creates members that are both the value and the identity, so comparisons, iteration, and validation behave predictably. Understanding how python enum values work — how they are assigned, accessed, and compared — prevents subtle bugs when you replace plain constants with enums.

Defining Enum Members with Explicit and Auto Values

A basic enum defines members as class attributes. Each member is an instance of the enum, and its value is whatever you assign.

from enum import Enum class Status(Enum): PENDING = 1 RUNNING = 2 DONE = 3

Here Status.PENDING is a Status member, not the integer 1. The integer is the member's value attribute. This distinction matters when you compare or serialize members.

If you do not care about specific numeric values, use auto() to let Python assign them sequentially starting from 1.

from enum import Enum, auto class Status(Enum): PENDING = auto() RUNNING = auto() DONE = auto()

auto() generates values based on the order of definition. The exact numbers are an implementation detail, so rely on the member names rather than the numeric values when using auto().

Accessing Members and Their Values

You can access a member by name or by value. The class attribute syntax is the most readable:

status = Status.RUNNING print(status.value) # 2

To look up a member from a value, call the enum class with the value:

status = Status(2) print(status) # Status.RUNNING

This lookup is case-sensitive and raises ValueError if no member matches. That behavior is useful for validating incoming data, but you must handle the exception when the input may be invalid.

def parse_status(code: int) -> Status: try: return Status(code) except ValueError: raise ValueError(f"{code} is not a valid status code") from None

If you need to look up by name, use Status["RUNNING"], which raises KeyError for unknown names. Choose the lookup method based on whether you have the name or the value.

Iterating and Comparing Enum Values

Iteration over an enum yields members in definition order, ignoring aliases by default.

for member in Status: print(member.name, member.value)

This gives you a predictable order, which is useful for generating dropdowns or API response schemas.

Enum members are singletons. Two references to the same member are the same object, so identity comparison works:

status = Status.RUNNING status is Status.RUNNING # True

Equality also works because Enum implements __eq__ based on the member identity. However, comparing a member to its raw value does not work by default:

Status.RUNNING == 2 # False

This is a common point of confusion. If you need to compare against the raw value, access .value explicitly or define a __eq__ that handles it. In most cases, it is better to keep the enum as the single source of truth and avoid mixing raw values in comparisons.

Using Aliases and Unique Values

By default, two members can share the same value. The second definition becomes an alias for the first.

class Status(Enum): PENDING = 1 WAITING = 1 # alias for PENDING

Iteration will only yield PENDING, not WAITING. Aliases are useful when you need multiple names for the same state, but they can hide duplicate values in your data model. To prevent accidental aliases, use the @unique decorator:

from enum import Enum, unique @unique class Status(Enum): PENDING = 1 WAITING = 1 # raises ValueError

This forces every value to be distinct, which is often the right choice for API contracts or database mappings where each value should have a single meaning.

Storing Additional Data with Enum Values

Sometimes a simple integer or string is not enough. You may want to attach a human-readable label, a color, or a database code to each member. You can do this by defining a custom enum class with extra attributes.

from enum import Enum class Status(Enum): PENDING = (1, "Queued") RUNNING = (2, "In progress") DONE = (3, "Completed") def __init__(self, code, label): self.code = code self.label = label

Now each member carries both the original value (the tuple) and the parsed attributes. Accessing Status.PENDING.code gives 1, and Status.PENDING.label gives "Queued". The value attribute still holds the full tuple, which may be undesirable if you only care about the code. To avoid that, you can override __new__ to set the value to just the code and store the label separately.

from enum import Enum class Status(Enum): def __new__(cls, code, label): obj = object.__new__(cls) obj._value_ = code obj.label = label return obj PENDING = (1, "Queued") RUNNING = (2, "In progress") DONE = (3, "Completed")

With this pattern, Status.PENDING.value is 1, and Status.PENDING.label is "Queued". This is a common approach when you need a clean value for serialization and a separate display name.

Handling Invalid Values and Edge Cases

When you receive data from an external system, you often need to validate that it corresponds to a known enum value. The constructor-based lookup is the cleanest way, but it raises ValueError for unknown values. In a production service, you should catch that exception and return a meaningful error instead of letting it propagate as a generic failure.

def get_status(code: int) -> Status | None: try: return Status(code) except ValueError: return None

Another edge case is when the enum value is a string that may contain whitespace or different casing. The built-in lookup is exact, so you may need to normalize input before calling the constructor. For example:

def parse_status(raw: str) -> Status | None: try: return Status[raw.strip().upper()] except KeyError: return None

This approach uses name-based lookup, which is case-sensitive. If your enum values are strings and you want to match on the value rather than the name, use the constructor with a normalized value.

Performance and Memory Considerations

Enums are implemented as classes, and each member is a singleton instance. Accessing a member by attribute is a class attribute lookup, which is fast and has no per-access allocation. The main cost is at class definition time, where each member is created once. For typical application use, this overhead is negligible.

When you use auto(), the values are generated sequentially, but the generation happens at class creation. There is no runtime cost for using auto() versus explicit values.

One performance-relevant practice is to avoid repeatedly constructing an enum from a value in a hot loop if the input is already a member. For example, if you have a list of integers that you convert to enum members, the conversion is a dictionary lookup internally, which is O(1). But if you can keep the data as enum members from the start, you avoid the conversion entirely.

Memory usage is also minimal: each member is a single object, and the class holds references to them. If you have a large number of enum members (thousands), the class creation time and memory footprint may become noticeable, but that is an unusual scenario for most applications.

Choosing Between Enum and Plain Constants

Enums are not always the right choice. For a small set of constants that are only used internally and never serialized, a module-level constant may be simpler:

PENDING = 1 RUNNING = 2 DONE = 3

However, plain constants lack the namespace and type safety that enums provide. With enums, you cannot accidentally pass a Status to a function that expects a different enum type, and the member names are grouped under a single class. This becomes valuable when the set of values is part of an API contract, a database schema, or a state machine.

Use an enum when:

  • The set of values is fixed and known at development time.
  • You need to iterate over all possible values.
  • You want to attach metadata to each value.
  • You need to validate incoming data against a known set.

Use plain constants when the values are truly arbitrary and not part of a closed set, or when the overhead of defining a class feels disproportionate to the task. In most professional codebases, enums are preferred for any set of related named values that appear in multiple places.

Serialization and Deserialization Patterns

A common production need is converting enum members to JSON and back. The default json module does not serialize enums directly, so you must define a custom encoder or convert to a primitive value.

import json from enum import Enum class Status(Enum): PENDING = 1 RUNNING = 2 DONE = 3 status = Status.RUNNING json.dumps(status) # raises TypeError

A simple approach is to serialize the .value attribute:

payload = {"status": status.value} json.dumps(payload)

When reading the JSON back, use the constructor to convert the integer to an enum member. This pattern keeps the wire format simple and stable. If you need to serialize the name instead, use status.name, but be aware that names are more likely to change than values in a well-designed enum.

For more complex enums with additional attributes, you may want to serialize the member as an object. In that case, define a method on the enum that returns a dictionary, and use a custom JSON encoder that calls it.

class Status(Enum): PENDING = (1, "Queued") RUNNING = (2, "In progress") DONE = (3, "Completed") def __new__(cls, code, label): obj = object.__new__(cls) obj._value_ = code obj.label = label return obj def to_dict(self): return {"code": self.value, "label": self.label}

This keeps the serialization logic close to the enum definition, making it easy to maintain when the enum changes.

Compatibility with Python Versions

The enum module has been part of the standard library since Python 3.4. The auto() function and the @unique decorator are available from the start. Later versions added minor conveniences, such as StrEnum and IntEnum in Python 3.11, which are subclasses of str and int respectively. If you need an enum whose members can be used directly as strings or integers, those classes can simplify code that interacts with external systems expecting primitive types.

from enum import IntEnum class Status(IntEnum): PENDING = 1 RUNNING = 2 DONE = 3 # Status.RUNNING == 2 is True

Using IntEnum or StrEnum changes the equality semantics, which can be convenient but also reduces type safety. Decide based on whether you need the enum to behave like its underlying type in comparisons and arithmetic. For most domain models, a plain Enum is safer because it prevents accidental mixing with primitive values.

When you upgrade Python versions, review whether the enum definitions still behave as expected, especially if you relied on the order of auto() values or on aliases. The enum module has been stable, but new features like StrEnum can affect how you design new enums.

A Practical Pattern for Validation and State Transitions

A common use case is a state machine where transitions are only allowed between certain states. Enums make the valid states explicit, and you can define a method that checks whether a transition is allowed.

from enum import Enum class Status(Enum): PENDING = 1 RUNNING = 2 DONE = 3 FAILED = 4 def can_transition_to(self, new_status: "Status") -> bool: allowed = { Status.PENDING: {Status.RUNNING, Status.FAILED}, Status.RUNNING: {Status.DONE, Status.FAILED}, Status.DONE: set(), Status.FAILED: set(), } return new_status in allowed[self]

This centralizes the transition rules in the enum itself, making it easier to test and reuse. The method uses the enum members as dictionary keys, which works because members are hashable and unique.

When you receive a request to change state, validate both the current state and the target state before applying the change. This pattern reduces the risk of invalid state changes across different parts of the application.

Final Considerations for Maintainability

Enums are a form of documentation. A well-named enum makes the allowed values visible at a glance, and the Python interpreter enforces that only defined members exist. When you change an enum, the compiler or runtime will catch references to removed members, which is safer than relying on string or integer constants that can silently pass through.

Keep enum definitions in a single module if they are shared across multiple modules, and avoid importing them in a way that creates circular dependencies. Since enums are classes, they are evaluated at import time, so be careful with forward references in type hints. Use string annotations or from __future__ import annotations if needed.

Finally, resist the urge to add methods to enums that are not directly related to the enum's domain. If you need complex business logic, place it in a service or a helper function that takes the enum as a parameter. This keeps the enum definition focused and maintainable.

python enum values: Practical Usage and Code Examples | RYUSLOG DEV