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)
.value vs Named PropertiesThe .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
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
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
| Approach | Immutable | Type-Safe | IDE Support | Best For |
|---|---|---|---|---|
Enum with __init__ | Yes | Yes | Yes | Fixed set of complex constants |
@dataclass | No | Yes | Yes | Mutable data containers with many instances |
NamedTuple | Yes | Yes | Yes | Immutable records, many instances |
| Plain dictionary | No | No | No | Dynamic 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.