Skip to content

How to Create Python Enums with Multiple Properties

Learn how to extend Python enums with multiple properties using tuples, dictionaries, and custom methods for rich, self-documenting constants with computed attributes and reverse lookups.

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:

(255, 0, 0)
rgb(255, 0, 0)

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

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}"

    @classmethod
    def from_symbol(cls, symbol):
        for currency in cls:
            if currency.symbol == symbol:
                return currency


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

Output:

None

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.

Navigation