Skip to main content

How to Create Python Enums with Multiple Properties

Standard Python enums map names to single values, which is useful for simple constants but limiting when each member needs to carry additional information. By overriding __init__, you can attach multiple named properties to each enum member, creating rich, self-documenting constants that are immutable and type-safe.

In this guide, you will learn how to create enums with multiple properties using tuples and dictionaries, add computed properties and helper methods, implement reverse lookup patterns, and understand when to choose enums over alternatives like dataclasses or named tuples.

Basic Multi-Property Enum

To give each enum member multiple properties, assign tuples as values and unpack them in a custom __init__ method:

from enum import Enum

class Planet(Enum):
MERCURY = (3.303e+23, 2.4397e6)
VENUS = (4.869e+24, 6.0518e6)
EARTH = (5.976e+24, 6.37814e6)
MARS = (6.421e+23, 3.3972e6)

def __init__(self, mass, radius):
self.mass = mass # in kilograms
self.radius = radius # in meters

@property
def surface_gravity(self):
G = 6.67300e-11 # gravitational constant
return G * self.mass / (self.radius ** 2)

print(Planet.EARTH.mass)
print(Planet.EARTH.radius)
print(Planet.EARTH.surface_gravity)

Output:

5.976e+24
6378140.0
9.802652743337129

The __init__ method receives the unpacked tuple values as arguments, and each one is stored as a named attribute on the enum member. The surface_gravity property is computed on the fly from the stored mass and radius values.

Color Enum with RGB and Hex

A practical example that stores multiple representations of a color in a single enum member:

from enum import Enum

class Color(Enum):
RED = (255, 0, 0, "#FF0000")
GREEN = (0, 255, 0, "#00FF00")
BLUE = (0, 0, 255, "#0000FF")
WHITE = (255, 255, 255, "#FFFFFF")
BLACK = (0, 0, 0, "#000000")

def __init__(self, r, g, b, hex_code):
self.r = r
self.g = g
self.b = b
self.hex_code = hex_code

@property
def rgb(self):
return (self.r, self.g, self.b)

def as_css(self):
return f"rgb({self.r}, {self.g}, {self.b})"

print(Color.RED.hex_code)
print(Color.RED.rgb)
print(Color.RED.as_css())

Output:

#FF0000
(255, 0, 0)
rgb(255, 0, 0)
Accessing .value vs Named Properties

The .value attribute still contains the original tuple. Named properties provide much clearer access without needing to remember index positions:

print(Color.RED.value)      # (255, 0, 0, '#FF0000')
print(Color.RED.hex_code) # #FF0000 (much clearer!)

HTTP Status Codes Example

Enums are an excellent fit for representing status codes that carry a numeric code, a short phrase, and a longer description:

from enum import Enum

class HTTPStatus(Enum):
OK = (200, "OK", "Request succeeded")
CREATED = (201, "Created", "Resource created")
BAD_REQUEST = (400, "Bad Request", "Invalid syntax")
NOT_FOUND = (404, "Not Found", "Resource not found")
SERVER_ERROR = (500, "Internal Server Error", "Server failed")

def __init__(self, code, phrase, description):
self.code = code
self.phrase = phrase
self.description = description

def __str__(self):
return f"{self.code} {self.phrase}"

status = HTTPStatus.NOT_FOUND
print(status)
print(status.code)
print(status.description)

Output:

404 Not Found
404
Resource not found

Overriding __str__ lets you control how the enum member is displayed when converted to a string, which is useful for logging and user-facing messages.

Using Dictionaries for Many Properties

When an enum member has many properties, long tuples become hard to read because you lose track of which position maps to which attribute. Using dictionaries at the definition site improves clarity:

from enum import Enum

class Country(Enum):
USA = {"code": "US", "capital": "Washington D.C.", "population": 331_000_000}
UK = {"code": "GB", "capital": "London", "population": 67_000_000}
JP = {"code": "JP", "capital": "Tokyo", "population": 126_000_000}

def __init__(self, data):
self.code = data["code"]
self.capital = data["capital"]
self.population = data["population"]

print(Country.JP.capital)
print(Country.JP.population)

Output:

Tokyo
126000000

With dictionaries, you can immediately see which value corresponds to which property at the definition site, making the code easier to maintain as the number of properties grows.

Adding Lookup Methods

A common need is finding an enum member by one of its properties rather than its name. Class methods provide clean reverse lookups:

from enum import Enum

class Currency(Enum):
USD = ("$", "US Dollar", 2)
EUR = ("€", "Euro", 2)
JPY = ("¥", "Japanese Yen", 0)
BTC = ("₿", "Bitcoin", 8)

def __init__(self, symbol, full_name, decimals):
self.symbol = symbol
self.full_name = full_name
self.decimals = decimals

@classmethod
def from_symbol(cls, symbol):
"""Find a currency by its symbol."""
for currency in cls:
if currency.symbol == symbol:
return currency
raise ValueError(f"Unknown currency symbol: {symbol}")

def format_amount(self, amount):
"""Format a numeric amount with the currency symbol."""
return f"{self.symbol}{amount:,.{self.decimals}f}"

yen = Currency.from_symbol("¥")
print(yen.full_name)
print(Currency.USD.format_amount(1234.5))
print(Currency.JPY.format_amount(1234.5))

Output:

Japanese Yen
$1,234.50
¥1,234
note

The @classmethod decorator allows methods that operate on the enum class itself rather than a specific member. This enables factory patterns and lookups that iterate over all members to find a match.

A Common Mistake: Forgetting Error Handling in Lookups

If you write a lookup method without handling the case where no member matches, the method silently returns None (or falls through without returning anything):

from enum import Enum

class Currency(Enum):
USD = ("$", "US Dollar", 2)
EUR = ("€", "Euro", 2)
JPY = ("¥", "Japanese Yen", 0)
BTC = ("₿", "Bitcoin", 8)

def __init__(self, symbol, full_name, decimals):
self.symbol = symbol
self.full_name = full_name
self.decimals = decimals

def format_amount(self, amount):
"""Format a numeric amount with the currency symbol."""
return f"{self.symbol}{amount:,.{self.decimals}f}"

# Currency class above is as before, except for the from_symbol method
@classmethod
def from_symbol(cls, symbol):
for currency in cls:
if currency.symbol == symbol:
return currency
# No match found, but no error raised either


result = Currency.from_symbol("£")
print(result)

Output:

None
note

This None return can cause confusing errors later in your code. Always raise a ValueError (as shown in the complete example above) when the lookup fails, so the problem surfaces immediately at the point of the incorrect call.

Iterating Over Multi-Property Enums

Since enums are iterable, you can loop over all members and access their properties:

from enum import Enum

class HTTPStatus(Enum):
OK = (200, "OK", "Request succeeded")
NOT_FOUND = (404, "Not Found", "Resource not found")
SERVER_ERROR = (500, "Internal Server Error", "Server failed")

def __init__(self, code, phrase, description):
self.code = code
self.phrase = phrase
self.description = description

for status in HTTPStatus:
print(f"{status.code}: {status.phrase}")

Output:

200: OK
404: Not Found
500: Internal Server Error

This is useful for generating documentation, building dropdown menus, or validating input against all known values.

Comparison with Alternatives

ApproachImmutableType-SafeIDE SupportBest For
Enum with __init__YesYesYesFixed set of complex constants
@dataclassNoYesYesMutable data containers with many instances
NamedTupleYesYesYesImmutable records, many instances
Plain dictionaryNoNoNoDynamic or runtime configuration

Choose enums when you have a fixed, known set of members that should not change at runtime. If you need to create many instances dynamically or mutate their values, dataclasses or named tuples are better suited.

Conclusion

Overriding __init__ in an enum class transforms simple name-value mappings into rich, self-documenting constants. Each member can have multiple named properties, computed attributes via @property, helper methods for formatting or conversion, and class methods for reverse lookups. This pattern works well for fixed sets like colors, status codes, planets, or currencies where you need more than a single value per member. Use tuple-based definitions for members with a few properties, and switch to dictionary-based definitions when the number of properties grows large enough that positional arguments become unclear.