Python @unique: Ensuring No Two Members in an Enum Class Have the Same Value

Python's @unique decorator gives you a single, explicit guarantee: no two members in an Enum class will share the same value. Place it above your class definition, and Python raises a ValueError at class creation time the moment it detects a duplicate. Without it, Python silently creates an alias instead of raising an error — a behavior that is correct by design but easy to misread as a bug.

Python's enum module was added in Python 3.4 via PEP 435, bringing named constants with real type identity into the standard library. One thing the module does not enforce by default is value uniqueness across members. Two members can legally share the same value, and Python handles that by treating the second name as an alias for the first. This is deliberate — aliases are useful in certain API normalization scenarios. But in the majority of cases, a programmer who accidentally types the same integer twice for two different members has made a mistake, not a design choice. That is the gap @unique closes.

The decision to keep aliases opt-out rather than opt-in was explicitly articulated during CPython development. Core developer Nick Coghlan, who proposed @unique in CPython issue 18042, reasoned that because aliases are consistent with how Python namespaces and dictionaries behave generally, it made more sense to provide an opt-in mechanism for stricter checks rather than change the default. In his own words, the decorator approach is "a better solution" precisely because the strictness is visible and deliberate at the call site. (Nick Coghlan, CPython issue tracker, issue 18042, 2013.)

That reasoning is why aliases exist without any flag or configuration option, and why @unique is a class decorator rather than a metaclass parameter. It is an explicit, visible opt-in at the call site — not a hidden default. For a broader look at how class decorators differ from metaclasses in Python, see Python class decorators vs metaclasses.

How Python Enum Aliases Work Without @unique

Before using @unique, it helps to understand what Python does when two members share a value. The behavior is controlled by the EnumType metaclass (called EnumMeta prior to Python 3.11, when it was renamed), which processes member definitions during class creation. When it encounters a value that has already been assigned to a prior member, it does not create a new object. Instead, it binds the new name to the existing object and marks that name as an alias in __members__.

from enum import Enum

class Color(Enum):
    RED = 1
    CRIMSON = 1   # alias — same value as RED
    SALMON = 1    # alias — same value as RED
    GREEN = 2
    BLUE = 3

In this class, CRIMSON and SALMON are not independent members. They are aliases for RED. Python creates only one object for value 1, and both names point to it:

print(Color.CRIMSON is Color.RED)   # True
print(Color.SALMON is Color.RED)    # True
print(Color.CRIMSON)                # Color.RED  (not Color.CRIMSON)
print(Color(1))                     # Color.RED

Notice that accessing Color.CRIMSON returns Color.RED. The alias name does not appear in the string representation. When you iterate the enum, only canonical members appear — aliases are skipped:

for color in Color:
    print(color)

# Color.RED
# Color.GREEN
# Color.BLUE
# CRIMSON and SALMON do NOT appear

To see all names including aliases, use the __members__ property, which is a read-only ordered mapping of every name — canonical and alias — to its member object:

Python Pop Quiz
Given this enum — how many items does list(Color) return?

class Color(Enum):
    RED = 1
    CRIMSON = 1
    SALMON = 1
    GREEN = 2
    BLUE = 3
from pprint import pprint
pprint(Color.__members__)

# mappingproxy({
#   'RED':     <Color.RED: 1>,
#   'CRIMSON': <Color.RED: 1>,   # alias — same object
#   'SALMON':  <Color.RED: 1>,   # alias — same object
#   'GREEN':   <Color.GREEN: 2>,
#   'BLUE':    <Color.BLUE: 3>
# })

The output makes the aliasing explicit: CRIMSON and SALMON both reference the same object as RED. This is the correct Python behavior when aliases exist. There are real scenarios where this is useful. Consider an API that returns status codes from two different backend systems with different names but semantically identical meanings:

from enum import Enum

class ResponseStatus(Enum):
    # in-progress states
    IN_PROGRESS = 1
    REQUESTING  = 1
    PENDING     = 1
    # success states
    SUCCESS     = 2
    OK          = 2
    FULFILLED   = 2
    # error states
    ERROR       = 3
    NOT_OK      = 3
    REJECTED    = 3

# A string from either system resolves to the canonical member
code = "FULFILLED"
if ResponseStatus[code] is ResponseStatus.SUCCESS:
    print("Request completed successfully.")  # prints

This pattern lets you normalize terminology from multiple sources against a single authoritative name per state. The alias lookup via ResponseStatus["FULFILLED"] returns ResponseStatus.SUCCESS automatically.

Note

Aliases are intentionally supported by the enum module. The problem is not that aliases exist — it is that they can appear accidentally when you meant to write distinct values. @unique exists to catch those accidents.

The alias scenario works because Python creates a new name pointing to the same object rather than ignoring the declaration entirely. But what looks like a separate member in the class body is not one. That silent collapsing of names is the exact behavior that @unique prevents when you do not want it.

What @unique Does and How It Works Internally

@unique is a class decorator exported from the enum module. It takes an enum class as its argument, inspects its __members__ mapping for any aliases, and raises a ValueError if it finds any. The check happens at class definition time — not at runtime when members are accessed. That means a badly-defined enum will fail immediately on import, before any code that uses it can run.

from enum import Enum, unique

@unique
class Season(Enum):
    SPRING = 1
    SUMMER = 2
    AUTUMN = 3
    WINTER = 3  # duplicate value — will raise ValueError

Running this code raises:

ValueError: duplicate values found in <enum 'Season'>: WINTER -> AUTUMN

The error message names the alias (WINTER) and the canonical member it would have pointed to (AUTUMN). Internally, unique() identifies aliases by comparing the keys of __members__ against the set of canonical members returned by iterating the class directly. Any name in __members__ that is not present in the canonical iteration is an alias. The function collects all such pairs and formats them into the error string before raising.

Python Pop Quiz
When exactly does @unique raise its ValueError?

Importing it is straightforward. You can import unique directly from enum or use it as enum.unique after importing the module:

# Option 1 — direct import
from enum import Enum, unique

@unique
class Direction(Enum):
    NORTH = 1
    SOUTH = 2
    EAST  = 3
    WEST  = 4
# Option 2 — module-level access
import enum
from enum import Enum

@enum.unique
class Direction(Enum):
    NORTH = 1
    SOUTH = 2
    EAST  = 3
    WEST  = 4

Both forms are equivalent. The direct import style is more common in practice because it keeps the decorator syntax clean.

Catching Duplicate Values in String Enums

Value types other than integers are subject to the same check. A typo in a string value is just as likely as one in an integer, and @unique catches it the same way:

from enum import Enum, unique

@unique
class Day(Enum):
    MON = "Monday"
    TUE = "Monday"   # accidentally duplicated — raises ValueError
    WED = "Wednesday"
    THU = "Thursday"
    FRI = "Friday"
    SAT = "Saturday"
    SUN = "Sunday"

Without @unique, TUE silently becomes an alias for MON, and iterating the enum produces only six items instead of seven. That type of bug can go undetected for a long time because the code does not crash — it simply produces incomplete results.

Multiple Aliases in a Single Error

When more than one alias is present, @unique reports all of them in a single ValueError, not just the first one found:

from enum import Enum, unique

@unique
class HttpMethod(Enum):
    GET    = "GET"
    POST   = "POST"
    PUT    = "PUT"
    PATCH  = "PUT"    # alias for PUT
    DELETE = "DELETE"
    HEAD   = "GET"    # alias for GET

This raises:

ValueError: duplicate values found in <enum 'HttpMethod'>: PATCH -> PUT, HEAD -> GET

All alias pairs are surfaced at once so you can fix the entire class without iterating through one error at a time.

Using @unique with auto()

The auto() function generates values automatically, and for a standard Enum class it increments the highest integer seen so far by one. When used correctly, auto() never produces duplicates on its own. However, mixing auto() with explicit values in the same class can create alias conditions that are harder to see at a glance. Pairing @unique with auto() adds a verification layer:

from enum import Enum, auto, unique

@unique
class State(Enum):
    PENDING    = auto()   # 1
    PROCESSING = auto()   # 2
    COMPLETE   = auto()   # 3
    FAILED     = auto()   # 4
    # All values generated by auto() are distinct — decorator passes silently
from enum import Enum, auto, unique

@unique
class State(Enum):
    PENDING    = auto()   # 1
    PROCESSING = auto()   # 2
    COMPLETE   = 2        # explicit value collides with PROCESSING — raises ValueError
    FAILED     = auto()   # never reached — class creation fails before this is evaluated

In that second example, COMPLETE = 2 creates an alias for PROCESSING because auto() already assigned 2 to PROCESSING. The @unique decorator catches the collision and raises a ValueError.

Pro Tip

When mixing auto() with hand-written values, add @unique as a safety net. The decorator costs nothing when all values are already distinct, and it surfaces collisions immediately at import time instead of producing subtle iteration bugs later.

@unique vs @verify(UNIQUE) — Python 3.11 and Later

Python 3.11 introduced a more flexible constraint system for enums via verify() and the EnumCheck enumeration. The @verify(UNIQUE) pattern covers the same ground as @unique but is composable with other checks in a single decorator call:

# Python 3.11+
from enum import Enum, verify, UNIQUE

@verify(UNIQUE)
class Color(Enum):
    RED     = 1
    GREEN   = 2
    BLUE    = 3
    CRIMSON = 1  # raises: aliases found in <enum 'Color'>: CRIMSON -> RED

Note the slightly different error message: verify(UNIQUE) says "aliases found" while the classic @unique says "duplicate values found." The semantics are the same — both reject any member whose value duplicates an existing member's value.

The real advantage of verify() is that it supports combining checks. The CONTINUOUS check ensures no integer values are missing between the lowest and highest member, and NAMED_FLAGS ensures that every combination value in a Flag enum has a corresponding named member. You can stack them:

# Python 3.11+ — enforce both uniqueness and no gaps
from enum import Enum, verify, UNIQUE, CONTINUOUS

@verify(UNIQUE, CONTINUOUS)
class Priority(Enum):
    LOW    = 1
    MEDIUM = 2
    HIGH   = 3
    # No duplicates and no gaps — passes both checks
# This fails CONTINUOUS because values 3 and 4 are missing
from enum import Enum, verify, UNIQUE, CONTINUOUS

@verify(UNIQUE, CONTINUOUS)
class Priority(Enum):
    LOW    = 1
    MEDIUM = 2
    HIGH   = 5  # raises: invalid enum 'Priority': missing values 3, 4

For code targeting Python 3.11 and later, @verify(UNIQUE) is the more composable choice. For code that needs to support older Python versions, @unique remains the right tool since it has been available since Python 3.4.

Python Pop Quiz
What does @verify(UNIQUE, CONTINUOUS) enforce that @unique alone cannot?
@unique
Python 3.4
@verify(UNIQUE)
Python 3.11
@unique
Yes — ValueError
@verify(UNIQUE)
Yes — ValueError
@unique
"duplicate values found"
@verify(UNIQUE)
"aliases found"
@unique
No
@verify(UNIQUE)
Yes (CONTINUOUS, NAMED_FLAGS)
@unique
Yes
@verify(UNIQUE)
Yes
@unique
Class definition time
@verify(UNIQUE)
Class definition time

@unique Across Enum Variants and Edge Cases

The @unique decorator works with every class that inherits from Enum, including the specialized subtypes. Understanding how it behaves with each gives you a clearer picture of when to reach for it.

IntEnum

IntEnum members compare equal to integers and other IntEnum members of different classes if their integer values match. This makes cross-class identity comparisons loose, which is one reason the Python documentation recommends preferring plain Enum for new code unless integer interoperability is specifically needed. @unique still works cleanly with IntEnum:

from enum import IntEnum, unique

@unique
class HttpStatus(IntEnum):
    OK                  = 200
    CREATED             = 201
    NO_CONTENT          = 204
    BAD_REQUEST         = 400
    UNAUTHORIZED        = 401
    NOT_FOUND           = 404
    INTERNAL_SERVER_ERR = 500

# Integer comparison works because it's IntEnum
response_code = 404
if response_code == HttpStatus.NOT_FOUND:
    print("Resource not found.")   # prints
# Duplicate value with @unique raises immediately
from enum import IntEnum, unique

@unique
class HttpStatus(IntEnum):
    OK          = 200
    SUCCESS     = 200   # raises ValueError: duplicate values found: SUCCESS -> OK
    NOT_FOUND   = 404

StrEnum (Python 3.11+)

StrEnum members behave like strings, which makes them convenient for use in contexts that expect a str — such as dictionary keys, log level names, or database column values. @unique applies equally. When member names map cleanly to their string values, the idiomatic pattern is to use auto(), which for StrEnum generates the lowercased member name automatically:

# Python 3.11+ — auto() generates lowercased member names for StrEnum
from enum import StrEnum, auto, unique

@unique
class LogLevel(StrEnum):
    DEBUG    = auto()   # "debug"
    INFO     = auto()   # "info"
    WARNING  = auto()   # "warning"
    ERROR    = auto()   # "error"
    CRITICAL = auto()   # "critical"

# StrEnum members compare directly to strings
level = "error"
if level == LogLevel.ERROR:
    print("Error-level event detected.")  # prints

# Values are the lowercased names
print(LogLevel.WARNING)   # warning
print(str(LogLevel.INFO)) # info

When string values must differ from the member name — for example to match an external API's casing or terminology — assign them explicitly instead of using auto():

# Python 3.11+ — explicit values when they differ from lowercased names
from enum import StrEnum, unique

@unique
class LogLevel(StrEnum):
    DEBUG    = "DEBUG"
    INFO     = "INFO"
    WARNING  = "WARN"    # differs from lowercased name
    ERROR    = "ERROR"
    CRITICAL = "CRIT"    # differs from lowercased name
# A typo causing a duplicate string value
from enum import StrEnum, unique

@unique
class LogLevel(StrEnum):
    DEBUG   = "debug"
    INFO    = "info"
    VERBOSE = "info"    # raises ValueError: duplicate values found: VERBOSE -> INFO
    ERROR   = "error"

@unique Does Not Inherit to Subclasses

One important behavior to understand is that @unique is not inherited. It runs once at class creation time for the class it decorates. If you create a subclass of a @unique-decorated enum, the subclass does not automatically inherit the uniqueness constraint:

from enum import IntEnum, unique

@unique
class UniqueBase(IntEnum):
    pass  # No members defined — @unique has nothing to check

class Extended(UniqueBase):
    ONE   = 1
    TWO   = 2
    THREE = 3
    FOUR  = 3   # alias — but NO error is raised because @unique is not on Extended

print(Extended.FOUR)   # Extended.THREE — silently aliased

This is a documented behavior, not a bug. The Python documentation states that @unique decorates the class it is applied to, not any classes derived from it. Each class that requires the constraint must carry its own @unique decorator:

from enum import IntEnum, unique

@unique
class Extended(IntEnum):
    ONE   = 1
    TWO   = 2
    THREE = 3
    FOUR  = 3   # now raises ValueError because @unique is on Extended

A Note on Flag Aliases

For Flag and IntFlag classes, Python uses an expanded definition of what counts as an alias. A flag member is canonical only if its value is a single power of two. Any member with a value of zero, or any member whose value is a combination of multiple powers of two, is treated as an alias — even if no other member shares that exact integer. This is because the Flag type is designed around bitwise composition, and composite values are expected to be derivable from named single-bit members rather than declared independently.

from enum import Flag, unique

# This raises immediately because READ_WRITE = 3 is a composite value
# (1 | 2) in a Flag class — @unique treats it as an alias
@unique
class Permission(Flag):
    READ       = 1
    WRITE      = 2
    READ_WRITE = 3   # ValueError: aliases found in <enum 'Permission'>: READ_WRITE -> READ|WRITE

If you need named composite flag values for convenience, omit @unique and use @verify(NAMED_FLAGS) instead (Python 3.11+), which checks for the opposite condition: that every combination value in the flag has an explicitly named member.

Practical Application: Role-Based Access Control

A common real-world use of @unique involves permission or role systems where each level must carry a distinct numeric weight. Accidentally assigning the same integer to two different roles would produce silent security logic errors — a user with EDITOR permissions could pass a check written for MODERATOR simply because they share a value:

from enum import IntEnum, unique

@unique
class UserRole(IntEnum):
    GUEST     = 0
    READER    = 10
    EDITOR    = 20
    MODERATOR = 30
    ADMIN     = 40
    SUPERUSER = 50

def require_role(user_role: UserRole, minimum: UserRole) -> bool:
    return user_role >= minimum

print(require_role(UserRole.EDITOR, UserRole.READER))    # True
print(require_role(UserRole.READER, UserRole.MODERATOR)) # False

If someone accidentally wrote EDITOR = 30 instead of 20, EDITOR would become an alias for MODERATOR, and every editor in the system would silently gain moderator-level access. The @unique decorator turns that silent error into an immediate crash at startup.

Practical Application: Database-Mapped Enum Values

When an enum maps directly to database column values, duplicate values cause lookup collisions. If two member names share a value, a query result containing that integer will always resolve to the canonical member name, not the alias — which means one of your code paths can never be reached:

from enum import IntEnum, unique

@unique
class OrderStatus(IntEnum):
    PENDING    = 1
    CONFIRMED  = 2
    SHIPPED    = 3
    DELIVERED  = 4
    CANCELLED  = 5
    REFUNDED   = 6

def get_status_label(db_value: int) -> str:
    try:
        status = OrderStatus(db_value)
        return status.name
    except ValueError:
        return "UNKNOWN"

print(get_status_label(3))   # SHIPPED
print(get_status_label(99))  # UNKNOWN

With @unique in place, you can trust that every integer stored in the database maps to exactly one member name. Without it, two column values could resolve to the same member and the other name would be unreachable by value lookup.

Catching Errors at Module Import Time

One of the most valuable properties of @unique is that it forces failures forward in time — to the moment the module is imported, not the moment a specific code path is exercised. This is particularly important in large applications where an enum might be defined in one module and consumed across dozens of others:

# statuses.py — this module fails on import if any duplicate values exist
from enum import Enum, unique

@unique
class TaskStatus(Enum):
    QUEUED     = "queued"
    RUNNING    = "running"
    PAUSED     = "running"   # error — will crash on import, not at runtime
    DONE       = "done"
    FAILED     = "failed"
# main.py
import statuses   # ValueError raised immediately here

# Output:
# ValueError: duplicate values found in <enum 'TaskStatus'>: PAUSED -> RUNNING

The error is deterministic. It does not depend on which branch of code is taken or which test is run first. Every time the module is imported, the check runs. That predictability is what makes @unique a useful addition to any enum definition where value uniqueness matters.

Warning

Do not rely on @unique to validate enum values coming from external data sources at runtime. The decorator only checks the class definition itself. If external data contains an unknown integer, Enum(bad_value) raises a ValueError — that is the correct mechanism for runtime validation, separate from @unique.

Wrapping @unique in a try/except for Debugging

In test environments or when building tooling that programmatically generates enums, you may want to catch the ValueError from @unique and inspect it rather than letting it propagate as a crash:

from enum import Enum, unique
from typing import Optional

def try_define_enum(member_map: dict) -> Optional[type]:
    try:
        cls = unique(Enum("GeneratedEnum", member_map))
        return cls
    except ValueError as exc:
        print(f"Enum definition rejected: {exc}")
        return None

result = try_define_enum({"A": 1, "B": 2, "C": 2})
# Output: Enum definition rejected: duplicate values found in <enum 'GeneratedEnum'>: C -> B

result = try_define_enum({"A": 1, "B": 2, "C": 3})
print(result)
# Output: <enum 'GeneratedEnum'>

This approach is particularly useful when enums are constructed from configuration files or database records, where you want to validate the definition before using it.

How @unique Interacts with Tuple-Valued Members

Python allows enum members to hold tuple values when the class defines a custom __new__ or __init__. The uniqueness check applies to the full tuple, comparing complete values rather than individual components:

from enum import Enum, unique

@unique
class Coordinate(Enum):
    ORIGIN    = (0, 0)
    UNIT_X    = (1, 0)
    UNIT_Y    = (0, 1)
    UNIT_DIAG = (1, 1)

    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y

print(Coordinate.UNIT_X.x)   # 1
print(Coordinate.UNIT_X.y)   # 0
from enum import Enum, unique

@unique
class Coordinate(Enum):
    ORIGIN      = (0, 0)
    UNIT_X      = (1, 0)
    ALSO_ORIGIN = (0, 0)  # raises ValueError: duplicate values found: ALSO_ORIGIN -> ORIGIN

    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y

The decorator compares the full tuple (0, 0) as the value and correctly identifies the collision.

Inside the Lookup: _value2member_map_ and @unique

Understanding why @unique works the way it does — and what it is actually protecting — requires looking one layer deeper into how Python's enum module stores and retrieves members at runtime. The EnumType metaclass maintains a private dict on every enum class called _value2member_map_. This is the data structure that makes value-based lookup fast.

When you call Color(1) or OrderStatus(3), Python is not iterating every member to find a match. It is doing a direct dictionary lookup against _value2member_map_, which maps each unique value to the canonical member that owns it. This makes enum lookup O(1). However, _value2member_map_ only stores one entry per value — the canonical member. Aliases are not given their own entry:

from enum import Enum

class Color(Enum):
    RED     = 1
    CRIMSON = 1   # alias
    GREEN   = 2
    BLUE    = 3

# _value2member_map_ contains only canonical members
print(Color._value2member_map_)
# {1: <Color.RED: 1>, 2: <Color.GREEN: 2>, 3: <Color.BLUE: 3>}

# CRIMSON is absent from the value map — looking up value 1 always returns RED
print(Color(1))   # Color.RED
print(Color(1) is Color.CRIMSON)   # True (same object)
print(Color(1) is Color.RED)       # True

This is the structural reason why aliases are invisible to value-based lookup. CRIMSON exists in __members__ as a name, but it has no entry in _value2member_map_. The moment you look up by value, you will always get RED.

Now consider what this means in practice. If your enum maps to database column values and you accidentally create an alias, the alias name becomes permanently unreachable by value. Any row that comes back from the database with value 1 will always resolve to RED, never to CRIMSON. Any code path that performs a comparison against Color.CRIMSON using is will still work — because both names point to the same object — but code that reads a string representation, logs the member name, or serializes the enum to JSON will always produce "RED", never "CRIMSON". The name you thought you were using is silently absorbed.

@unique prevents any alias from entering _value2member_map_ in the first place. It does not patch or alter the lookup structure — it simply refuses to let the class be constructed if any alias condition exists. The result is a guarantee that _value2member_map_ has exactly as many entries as the class has members, and that every name you defined is reachable by value.

from enum import Enum, unique

@unique
class Color(Enum):
    RED   = 1
    GREEN = 2
    BLUE  = 3

# With @unique, len(__members__) and len(_value2member_map_) are always equal
print(len(Color.__members__))        # 3
print(len(Color._value2member_map_)) # 3
# Every name is reachable by value — no silent absorptions
Diagnostic Tip

If you suspect an enum class has silently created aliases, compare len(YourEnum.__members__) against len(YourEnum._value2member_map_). A difference means aliases exist. You can also iterate YourEnum.__members__.items() and print any name where name != member.name — those are the aliases. This check can be added to a unit test without needing @unique on the production class itself.

Python Pop Quiz
An enum has members RED=1, CRIMSON=1, GREEN=2, BLUE=3. What does Color._value2member_map_ contain?

Sources

How to Enforce Unique Values in a Python Enum Class

Applying @unique to an enum class takes four steps. The check fires at class definition time, so errors surface on import — not in production.

  1. Import unique from the enum module. Add from enum import Enum, unique at the top of your file. The @unique decorator is part of the standard library and requires no additional installation.
  2. Apply @unique on the line immediately before the class statement. The decorator runs at class definition time, meaning the uniqueness check fires on import before any code can use the enum.
  3. Define members with distinct values. Assign a unique value to each member. If any two members share a value, @unique raises a ValueError naming the alias and the canonical member it would have resolved to.
  4. For Python 3.11+, consider @verify(UNIQUE) for composable constraints. If your codebase targets Python 3.11 or later, @verify(UNIQUE) covers the same ground while also supporting CONTINUOUS and NAMED_FLAGS checks in a single decorator call.

Frequently Asked Questions

What does the @unique decorator do in Python?

The @unique decorator checks an enumeration's __members__ for aliases — names that map to the same value as an existing member. If any aliases are found, it raises a ValueError at class definition time, preventing the class from being created. This enforces a one-to-one relationship between member names and their values.

What is an enum alias in Python?

An enum alias is a member name that shares the same value as a previously defined member in the same Enum class. Python silently creates aliases by default rather than raising an error. The alias resolves to the original member at lookup time, meaning the two names point to the exact same object.

What is the difference between @unique and @verify(UNIQUE)?

@unique is the classic decorator available since Python 3.4. @verify(UNIQUE) was introduced in Python 3.11 as part of the broader verify() system, which also supports CONTINUOUS and NAMED_FLAGS checks. Both raise a ValueError for duplicate values, but @verify(UNIQUE) produces a slightly different error message ("aliases found" vs "duplicate values found") and can be combined with other EnumCheck constraints in a single decorator call.

Does @unique apply to subclasses of an Enum?

No. @unique only applies to the class it decorates at definition time. If you apply @unique to a base class and then subclass it, the constraint is not inherited. Each subclass that requires unique value enforcement must be independently decorated with @unique.

Can @unique be used with IntEnum and StrEnum?

Yes. @unique works with any class that inherits from Enum, including IntEnum, StrEnum, Flag, and IntFlag. The decorator checks __members__ regardless of the underlying value type, so it enforces uniqueness across integer-valued and string-valued enumerations equally.

Can @unique be used with the Enum functional API?

Yes. Because @unique is a plain callable that accepts an enum class and returns it (or raises), it can be called directly rather than used as decorator syntax: cls = unique(Enum('MyEnum', member_map)). This is useful in testing or tooling scenarios where enums are constructed programmatically from dictionaries or configuration data.

Key Takeaways

  1. Aliases exist by design, not by accident: Python's Enum metaclass deliberately supports multiple names pointing to the same value. @unique is not fixing a flaw in the language — it is opting out of a feature that is not always desirable.
  2. Errors fire at class definition time: Duplicate values under @unique raise a ValueError the moment the class is defined, not when its members are accessed. This moves bugs as early as possible in the execution lifecycle, making them harder to miss.
  3. The decorator is not inherited: Applying @unique to a base class does not enforce uniqueness in subclasses. Each subclass must carry its own @unique decorator if the constraint is required.
  4. For Python 3.11+, consider @verify(UNIQUE): The newer verify() system allows combining uniqueness enforcement with other constraints such as CONTINUOUS and NAMED_FLAGS in a single decorator call, making it the more composable option for modern codebases.
  5. Aliases vanish from value lookup: The internal _value2member_map_ dict stores only canonical members. An alias name is reachable by attribute access and through __members__, but is permanently invisible to value-based lookup. @unique guarantees that len(__members__) equals len(_value2member_map_) — every name you defined can be reached by its value.

The @unique decorator is one of the smallest tools in the Python standard library and one of the most precise. It does exactly one thing — it refuses to let a class be created if any member would silently resolve to an alias — and that single behavior prevents an entire category of subtle, hard-to-detect bugs in systems that depend on enumerations for type safety, database mapping, permission modeling, or API normalization.