As was suggested in the comments, the enum
module provides a nice solution here. By mixing in str
with enum.Enum
, we can create an Enum
that is fully backwards-compatible with str
(i.e., can be used wherever a str
type is expected.
from enum import Enum
# The mixin type has to come first if you're combining
# enum.Enum with other types
class Values(str, Enum):
N1 = 'One'
N2 = 'Two'
N3 = 'Three'
If we enter this definition into the interactive console, we'll see that this class has the following behaviour:
>>> Values['N1']
<Values.N1: 'One'>
>>> Values('One')
<Values.N1: 'One'>
>>> Values.N1
<Values.N1: 'One'>
>>> Values('One') is Values.N1 is Values['N1']
True
>>> Values.N1.name
'N1'
>>> Values.N1.value
'One'
>>> Values.N1 == 'One'
True
>>> Values.N1 is 'One'
False
>>> Values.N1.startswith('On')
True
>>> type(Values.N1)
<enum 'Values'>
>>> for key in Values:
... print(key)
...
Values.N1
Values.N2
Values.N3
>>> list(Values)
[Values.N1, Values.N2, Values.N3]
As you can see, we have defined a new type which:
- Provides both dynamic dictionary-like access to members, as well as more type-safe forms of access.
- Is fully backwards-compatible with
str
— it can freely be compared with str
objects, and str
methods can be used on its members.
- Can be used in type hints as an alternative to
typing.Literal
. If I have a function like this:
def do_something(some_string):
if some_string not in ('One', 'Two', 'Three'):
raise Exception('NO.')
then I can either annotate it like this:
from typing import Literal
def do_something(some_string: Literal['One', 'Two', 'Three']) -> None:
...
or like this (in which case you'll have to pass in a member of the Values
enum rather than a vanilla string, or the type-checker will raise an error):
# Values enum as defined above
def do_something(some_string: Values) -> None:
...
There's an even more detailed guide to the intricacies of python Enum
s here.