composite-enum
Build superset enums by composing members from other enums. Included members become real first-class members of the new enum, with introspection back to their origin.
from enum import Enum
from composite_enum import CompositeEnum
class Operator(Enum):
UNION = "|"
INTERSECT = "&"
DIFF = "-"
SYM_DIFF = "^"
class TokenType(CompositeEnum, includes=Operator):
IDENT = "IDENT"
STRING = "STRING"
ASSIGN = "="
LPAREN = "("
RPAREN = ")"
# TokenType has all 9 members: 4 from Operator + 5 of its own
list(TokenType)
# [UNION, INTERSECT, DIFF, SYM_DIFF, IDENT, STRING, ASSIGN, LPAREN, RPAREN]
# Included members are real members
TokenType.UNION # <TokenType.UNION: '|'>
TokenType.UNION.value # '|'
TokenType("|") # <TokenType.UNION: '|'>
TokenType["UNION"] # <TokenType.UNION: '|'>
# But they know where they came from
TokenType.UNION.source_enum # <enum 'Operator'>
TokenType.UNION.to_source() # <Operator.UNION: '|'>
TokenType.from_source(Operator.UNION) # <TokenType.UNION: '|'>
TokenType.IDENT.source_enum # None (defined directly)
TokenType.members_from(Operator) # frozenset({UNION, INTERSECT, DIFF, SYM_DIFF})
Install
pip install composite-enum
Development
git clone https://github.com/isaacfuenmayora/composite-enum
cd composite-enum
With uv:
uv sync
uv run pytest
With pip:
pip install -e . && pip install pytest
pytest
Why
Python's Enum doesn't allow subclassing an enum that already has members.
This is intentional (docs),
but it means you can't express "TokenType is Operator plus some extra token
types" through inheritance. You end up duplicating the values and hoping
they stay in sync.
This restriction exists for good reason.
flufl.enum, the precursor to
Python's stdlib enum, supported member inheritance natively. That
feature was dropped in PEP 435 because it conflicts with members being
instances of their enum class. CPython core developer Alyssa Coghlan
later speculated
that extensible enums would require aggregating members from multiple
independent enumerations, sketching a hypothetical syntax:
class MoreColors(AggregateEnum, extends=Color):
cyan = ...
magenta = ...
This was never implemented in the stdlib. composite-enum takes a
similar approach using includes instead of extends.
composite-enum solves this with a metaclass that injects source enum
members into the new enum's namespace during class creation.
Usage
The opening example covers the basics. Here's what else you can do.
Multiple sources
class Delimiter(Enum):
COMMA = ","
SEMICOLON = ";"
class TokenType(CompositeEnum, includes=(Operator, Delimiter)):
IDENT = "IDENT"
STRING = "STRING"
ASSIGN = "="
LPAREN = "("
RPAREN = ")"
TokenType.included_enums() # (Operator, Delimiter)
# Included members appear first, in includes order, then class body
list(TokenType)
# [UNION, INTERSECT, DIFF, SYM_DIFF, COMMA, SEMICOLON, IDENT, STRING, ASSIGN, LPAREN, RPAREN]
With StrEnum / IntEnum
CompositeEnum can't be used alongside StrEnum or IntEnum
(Python's enum inheritance rules). Use the metaclass directly:
from enum import StrEnum # 3.11+
from composite_enum import CompositeEnumMeta
class TokenType(StrEnum, metaclass=CompositeEnumMeta, includes=Operator):
IDENT = "IDENT"
isinstance(TokenType.UNION, str) # True
The metaclass validates that included values match the target's data type. All introspection methods work the same either way.
The same metaclass approach works for any data type mixin, not just
StrEnum and IntEnum. Use (float, Enum), (bytes, Enum), or
any custom type:
class Voltage(Enum):
LOW = 3.3
HIGH = 5.0
class Signal(float, Enum, metaclass=CompositeEnumMeta, includes=Voltage):
GROUND = 0.0
isinstance(Signal.LOW, float) # True
Note: Type checkers have two limitations with the
metaclass=CompositeEnumMetaapproach:
- They may flag the
includeskeyword, since they don't infer class keywords from metaclass signatures. Add# type: ignore[call-arg]to suppress this.- The instance-level attributes
source_enumandto_source()won't be visible to type checkers, because the.pyistub declares these onCompositeEnum, not on arbitrary metaclass-created classes. The class-level methods (from_source(),members_from(),included_enums(),includes_enum()) work fine on both paths since they're declared on the metaclass. SubclassingCompositeEnumis the type-checker-friendly path:from_source()narrows toSelf | Noneandmembers_from()tofrozenset[Self].Both work correctly at runtime regardless. Note that type checkers cannot resolve dynamically injected member names (e.g.
TokenType.UNION) on either path. This is a general limitation of enum metaclasses, not specific tocomposite-enum.
Nested composition
Composing from an already-composite enum works. source_enum points
to the immediate source, not the original:
class Base(CompositeEnum, includes=Operator):
IDENT = "IDENT"
class Extended(CompositeEnum, includes=Base):
EXTRA = "extra"
Extended.UNION.source_enum # <enum 'Base'>, not Operator
API Reference
CompositeEnum
Base class for composition. Extend this instead of Enum.
CompositeEnumMeta
The metaclass powering composition. Use directly when you need
StrEnum, IntEnum, etc. as the base type.
includes (class keyword)
class TokenType(CompositeEnum, includes=Operator): # single source
class TokenType(CompositeEnum, includes=(Operator, Delimiter)): # multiple sources
A single Enum type or a sequence of them whose members should be included.
member.source_enum
TokenType.UNION.source_enum # <enum 'Operator'>
TokenType.IDENT.source_enum # None
The source enum this member was included from, or None.
member.to_source()
TokenType.UNION.to_source() # Operator.UNION
TokenType.IDENT.to_source() # None
Convert a composite member back to its source enum member. Returns
None for members defined directly on the composite.
cls.from_source(member)
TokenType.from_source(Operator.UNION) # TokenType.UNION
Convert a source enum member to its composite equivalent. Returns
None when there's no match.
cls.members_from(source)
TokenType.members_from(Operator)
# frozenset({TokenType.UNION, TokenType.INTERSECT, ...})
Returns a frozenset of members that originated from source.
cls.included_enums() / cls.includes_enum(source)
TokenType.included_enums() # (Operator, Delimiter)
TokenType.includes_enum(Operator) # True
Introspect which source enums were composed in.
Supported Enum Types
| Base type | Python | Supported | How |
|---|---|---|---|
Enum |
3.10+ | Yes | CompositeEnum base class |
StrEnum |
3.11+ | Yes | metaclass=CompositeEnumMeta |
IntEnum |
3.10+ | Yes | metaclass=CompositeEnumMeta |
str, Enum mixin |
3.10+ | Yes | metaclass=CompositeEnumMeta |
int, Enum mixin |
3.10+ | Yes | metaclass=CompositeEnumMeta |
Flag |
any | No | Bitwise semantics across unrelated Flags are ambiguous |
IntFlag |
any | No | Same as Flag |
Source enum types
Source enums (the ones in includes) can be any Enum, StrEnum, or
IntEnum. Their values must be compatible with the target's data type:
| Target type | Accepted source values |
|---|---|
Enum (plain) |
Anything |
StrEnum / str, Enum |
Must be str |
IntEnum / int, Enum |
Must be int |
Caveats
Implementation detail dependency. The metaclass injects members via
_EnumDict.__setitem__, which is an implementation detail of CPython's
enum module. It's been stable since Python 3.6 and is unlikely to
change, but it's not a guaranteed public API. Tested on 3.10 through
3.15.
Source members are not in the composite. Enum.__contains__
uses isinstance, so Operator.UNION in TokenType is False even
though TokenType.UNION exists with the same value. Use
TokenType.from_source(Operator.UNION) to check membership.
Reserved member names. The names source_enum, included_enums,
includes_enum, members_from, to_source, and from_source are
reserved by the metaclass. Using any of them as a member name raises TypeError at class creation.
Source methods don't transfer. Only member names and values are
composed. Methods, properties, and custom __init__ defined on a
source enum are not carried over to the composite.
Source enum aliases are preserved. If a source enum has aliases (multiple names for the same value), they transfer as aliases in the composite too:
class Source(Enum):
PRIMARY = 1
ALIAS = 1 # alias of PRIMARY
class Target(CompositeEnum, includes=Source):
EXTRA = "extra"
Target.PRIMARY # <Target.PRIMARY: 1>
Target["ALIAS"] # <Target.PRIMARY: 1> (alias, same as source)
Value aliases across sources. If two included sources share a value (different name, same value), the second name becomes an alias of the first. This is standard enum behavior, not composite-specific, but it has implications for introspection:
class A(Enum):
X = 1
class B(Enum):
Y = 1
class Combined(CompositeEnum, includes=(A, B)):
Z = 2
Combined.Y # <Combined.X: 1> (Y is an alias)
Combined.from_source(B.Y) # <Combined.X: 1>
Combined.from_source(B.Y).source_enum # <enum 'A'> (not B)
Combined.from_source(B.Y).to_source() # <A.X: 1> (not B.Y)
Combined.members_from(A) # frozenset({<Combined.X: 1>})
Combined.members_from(B) # frozenset({<Combined.X: 1>}) (same member)
Because Y is an alias for X, the canonical member's source_enum
always points to whichever source provided the canonical name (A),
regardless of which source you used in from_source(). Likewise,
members_from() returns the canonical member for both sources.
How It Works
The metaclass overrides __prepare__ and __new__:
-
__prepare__runs before the class body executes. It creates the standard_EnumDictnamespace, then injects each source enum's members vianamespace[name] = value._EnumDict.__setitem__registers these as member candidates. This means included members appear first in iteration order. -
The class body executes next, adding its own members. If a name collides with an already-injected member,
_EnumDictraisesTypeErrorimmediately. -
__new__builds the actual enum class viasuper().__new__(), then attaches metadata for introspection.
The result is a normal stdlib Enum. Standard tools like isinstance,
pickle, match/case, and list() all work exactly as they would
with any hand-written enum. The only additions are the introspection
methods (source_enum, to_source, etc.).
Alternatives
-
flufl.enum is the original Python enum package (predating the stdlib) and still supports member inheritance natively. If you want true subclassing where parent and child share member identity, and you don't need to stay on the stdlib
enum,flufl.enumis actively maintained and battle-tested since 2004. -
aenum by the stdlib
enummaintainer providesextend_enum()for adding members to an existing enum at runtime. If you need to modify enums you don't control,aenumis the mature, well-established choice. -
extendable-enum takes a decorator approach:
@inheritable_enummakes an existing enum subclassable (soclass Derived(Base):works directly), while@copy_enum_memberscopies members from one enum into a new, distinct class. -
unionenum.py is a clever gist that creates union enums where members retain their original type identity rather than becoming members of the new class.
composite-enum occupies a slightly different niche: declarative
composition of one or more source enums at class-definition time, with
source tracking and type compatibility checks. If one of the above fits
your use case better, use it.
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file composite_enum-0.1.0.tar.gz.
File metadata
- Download URL: composite_enum-0.1.0.tar.gz
- Upload date:
- Size: 22.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f815d5f38754e7e79d0be55393fa6a8e0939c1de826064fa0b4a424b3234a799
|
|
| MD5 |
5464c898db81cf6a39f9cf0aa7b70e46
|
|
| BLAKE2b-256 |
529cf1baf03b5211cd0ec714db078848bc2381785aa4ff73637acee3248649b0
|
Provenance
The following attestation bundles were made for composite_enum-0.1.0.tar.gz:
Publisher:
publish.yml on isaacfuenmayora/composite-enum
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
composite_enum-0.1.0.tar.gz -
Subject digest:
f815d5f38754e7e79d0be55393fa6a8e0939c1de826064fa0b4a424b3234a799 - Sigstore transparency entry: 2771438819
- Sigstore integration time:
-
Permalink:
isaacfuenmayora/composite-enum@f152e6049e5bd0a79972fa10e3484f891dffb035 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/isaacfuenmayora
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f152e6049e5bd0a79972fa10e3484f891dffb035 -
Trigger Event:
release
-
Statement type:
File details
Details for the file composite_enum-0.1.0-py3-none-any.whl.
File metadata
- Download URL: composite_enum-0.1.0-py3-none-any.whl
- Upload date:
- Size: 10.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
16080127bf2e6d44713e41c686030cc0b9f3f4c40b7baac0284a4c1e6ed7f2e4
|
|
| MD5 |
f74cf9bd062a255c2571a98b7dcac928
|
|
| BLAKE2b-256 |
b8a578807fc37b8ea298dd5c85e9fc93465dfb6b355f2c5bd4e2388f24f66daa
|
Provenance
The following attestation bundles were made for composite_enum-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on isaacfuenmayora/composite-enum
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
composite_enum-0.1.0-py3-none-any.whl -
Subject digest:
16080127bf2e6d44713e41c686030cc0b9f3f4c40b7baac0284a4c1e6ed7f2e4 - Sigstore transparency entry: 2771439144
- Sigstore integration time:
-
Permalink:
isaacfuenmayora/composite-enum@f152e6049e5bd0a79972fa10e3484f891dffb035 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/isaacfuenmayora
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f152e6049e5bd0a79972fa10e3484f891dffb035 -
Trigger Event:
release
-
Statement type: