[Python-ideas] Enum: determining if a value is valid

1,466 views
Skip to first unread message

Ethan Furman

unread,
Mar 12, 2021, 4:49:49 PM3/12/21
to python-ideas
A question that comes up quite a bit on Stackoverflow is how to test to see if a value will result in an Enum member, preferably without having to go through the whole try/except machinery.

A couple versions ago one could use a containment check:

if 1 in Color:

but than was removed as Enums are considered containers of members, not containers of the member values. It was also possible to define one's own `_missing_` method and have it return None or the value passed in, but that has also been locked down to either return a member or raise an exception.

At this point I see three options:

1) add a `get(value, default=None)` to EnumMeta (similar to `dict.get()`

2) add a recipe to the docs

3) do nothing

Thoughts?

--
~Ethan~
_______________________________________________
Python-ideas mailing list -- python...@python.org
To unsubscribe send an email to python-id...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at https://mail.python.org/archives/list/python...@python.org/message/N6C2ROUCSLEPPH34BTFQNQE4ZM63WUWC/
Code of Conduct: http://python.org/psf/codeofconduct/

Ricky Teachey

unread,
Mar 12, 2021, 5:51:59 PM3/12/21
to Ethan Furman, python-ideas
On Fri, Mar 12, 2021 at 4:52 PM Ethan Furman <et...@stoneleaf.us> wrote:
A question that comes up quite a bit on Stackoverflow is how to test to see if a value will result in an Enum member, preferably without having to go through the whole try/except machinery.

A couple versions ago one could use a containment check:

   if 1 in Color:

but than was removed as Enums are considered containers of members, not containers of the member values.  It was also possible to define one's own `_missing_` method and have it return None or the value passed in, but that has also been locked down to either return a member or raise an exception.

At this point I see three options:

1) add a `get(value, default=None)` to EnumMeta (similar to `dict.get()`

2) add a recipe to the docs

3) do nothing

Thoughts?

--
~Ethan~

Could this be an instance where match-case might become the canonical solution?

I'm probably getting the syntax wrong, but maybe it would be something like:

match value:
    case MyEnum():
        assert isinstance(value, MyEnum)
    case _:
         assert not isinstance(value, MyEnum)


---
Ricky.

"I've never met a Kentucky man who wasn't either thinking about going home or actually going home." - Happy Chandler



Ethan Furman

unread,
Mar 12, 2021, 6:01:48 PM3/12/21
to python-ideas
On 3/12/21 2:49 PM, Ricky Teachey wrote:
> On Fri, Mar 12, 2021 at 4:52 PM Ethan Furman wrote:

>> A question that comes up quite a bit on Stackoverflow is how to test
>> to see if a value will result in an Enum member, preferably without
>> having to go through the whole try/except machinery.

> Could this be an instance where match-case might become the canonical
> solution?
>
> I'm probably getting the syntax wrong, but maybe it would be something like:
>
> match value:
> case MyEnum():
> assert isinstance(value, MyEnum)
> case _:
> assert not isinstance(value, MyEnum)

The use case is when you have an unknown value that may or may not convert into an Enum member. So a three-member Enum would look something like:

```python
match value:
case MyEnum():
pass
case 1|2|3:
value = MyEnum(value)
case _:
handle_error_or_use_default()
```

Seven lines of code. try/except would be something like:

```python
try:
value = MyEnum(value)
except ValueError:
handle_error_or_use_default()
```

vs what I'm envisioning:

```python
value = MyEnum.get(value, some_default)
```

or maybe

```python
value = MyEnum.get(value)
if value is None:
handle_error()
```

--
~Ethan~
_______________________________________________
Python-ideas mailing list -- python...@python.org
To unsubscribe send an email to python-id...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at https://mail.python.org/archives/list/python...@python.org/message/Z7C36KHOFDDM6REA4PN6GPPYOJ22DPBY/

Guido van Rossum

unread,
Mar 12, 2021, 8:30:48 PM3/12/21
to Ethan Furman, python-ideas
On Fri, Mar 12, 2021 at 1:52 PM Ethan Furman <et...@stoneleaf.us> wrote:
A question that comes up quite a bit on Stackoverflow is how to test to see if a value will result in an Enum member, preferably without having to go through the whole try/except machinery.

A couple versions ago one could use a containment check:

   if 1 in Color:

but than was removed as Enums are considered containers of members, not containers of the member values.

Maybe you were a bit too quick in deleting it. Was there a serious bug that led to the removal? Could it be restored?
 
It was also possible to define one's own `_missing_` method and have it return None or the value passed in, but that has also been locked down to either return a member or raise an exception.

At this point I see three options:

1) add a `get(value, default=None)` to EnumMeta (similar to `dict.get()`

But the way to convert a raw value to an enum value is Color(1), not Color[1], so Color.get(1) seems inconsistent.

Maybe you can just change the constructor so you can spell this as Color(1, default=None) (and then check whether that's None)?
 
2) add a recipe to the docs

But what would the recipe say? Apparently you're looking for a one-liner, since you reject the try/except solution.
 
3) do nothing

Always a good option. :-) Where's that StackOverflow item? How many upvotes does it have?

--
--Guido van Rossum (python.org/~guido)

Ethan Furman

unread,
Mar 15, 2021, 1:53:26 PM3/15/21
to python-ideas
On 3/12/21 5:28 PM, Guido van Rossum wrote:
> On Fri, Mar 12, 2021 at 1:52 PM Ethan Furman wrote:

>> A question that comes up quite a bit on Stackoverflow is how to test
>> to see if a value will result in an Enum member, preferably without
>> having to go through the whole try/except machinery.
>>
>> A couple versions ago one could use a containment check:
>>
>> if 1 in Color:
>>
>> but than was removed as Enums are considered containers of members,
>> not containers of the member values.
>
> Maybe you were a bit too quick in deleting it. Was there a serious
> bug that led to the removal? Could it be restored?

Part of the reason is that there are really two ways to identify an
enum -- by name, and by value -- which should `__contains__` work with?

>> At this point I see three options:
>>
>> 1) add a `get(value, default=None)` to EnumMeta (similar to `dict.get()`
>
> But the way to convert a raw value to an enum value is Color(1), not
> Color[1], so Color.get(1) seems inconsistent.

Very good point.

> Maybe you can just change the constructor so you can spell this as
> Color(1, default=None) (and then check whether that's None)?

An interesting idea.

>> 2) add a recipe to the docs
>
> But what would the recipe say? Apparently you're looking for a one-liner,
> since you reject the try/except solution.

The recipe would be for a method that could be added to an Enum, such as:

@classmethod
def get_by_value(cls, value, default=None):
for member in cls:
if member.value == value:
return member
return default

>> 3) do nothing
>
> Always a good option. :-)

Yes, but not always a satisfying one. :)

> Where's that StackOverflow item? How many upvotes does it have?


93 - How do I test if int value exists in Python Enum without using try/catch?
https://stackoverflow.com/q/43634618/208880

25 - How to test if an Enum member with a certain name exists?
https://stackoverflow.com/q/29795488/208880

3 - Validate value is in Python Enum values
https://stackoverflow.com/q/54126570/208880

2 - How to check if string exists in Enum of strings?
https://stackoverflow.com/q/63335753/208880

I think I like your constructor change idea, with a small twist:

Color(value=<sentinel>, name=<sentinel>, default=<sentinal>)

This would make it possible to search for an enum by value or by name, and also specify a default return value (raising an exception if the default is not set and a member cannot be found).

--
~Ethan~
_______________________________________________
Python-ideas mailing list -- python...@python.org
To unsubscribe send an email to python-id...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at https://mail.python.org/archives/list/python...@python.org/message/LJXMAORK2JVYTPQQ332NFEV2JPVFVJAX/

Guido van Rossum

unread,
Mar 15, 2021, 2:29:45 PM3/15/21
to Ethan Furman, python-ideas
On Mon, Mar 15, 2021 at 10:53 AM Ethan Furman <et...@stoneleaf.us> wrote:
On 3/12/21 5:28 PM, Guido van Rossum wrote:
> On Fri, Mar 12, 2021 at 1:52 PM Ethan Furman wrote:

>> A question that comes up quite a bit on Stackoverflow is how to test
>> to see if a value will result in an Enum member, preferably without
>> having to go through the whole try/except machinery.
>>
>> A couple versions ago one could use a containment check:
>>
>>     if 1 in Color:
>>
>> but than was removed as Enums are considered containers of members,
>> not containers of the member values.
>
> Maybe you were a bit too quick in deleting it. Was there a serious
> bug that led to the removal? Could it be restored?

Part of the reason is that there are really two ways to identify an
enum -- by name, and by value -- which should `__contains__` work with?

The two sets don't overlap, so we could allow both. (Funny interpretations of `__contains__` are not unusual, e.g. substring checks are spelled 'abc' in 'fooabcbar'.)
 
>> At this point I see three options:
>>
>> 1) add a `get(value, default=None)` to EnumMeta (similar to `dict.get()`
>
> But the way to convert a raw value to an enum value is Color(1), not
> Color[1], so Color.get(1) seems inconsistent.

Very good point.

> Maybe you can just change the constructor so you can spell this as
> Color(1, default=None) (and then check whether that's None)?

An interesting idea.

>> 2) add a recipe to the docs
>
> But what would the recipe say? Apparently you're looking for a one-liner,
> since you reject the try/except solution.

The recipe would be for a method that could be added to an Enum, such as:

     @classmethod
     def get_by_value(cls, value, default=None):
         for member in cls:
             if member.value == value:
                 return member
         return default

But that's a non-solution -- people can figure out how to write such a helper just fine (although probably using try/except) but they don't want to -- they have *one* line where they want to do this check and so they're going for a local solution -- probably the try/except.
 
>> 3) do nothing
>
> Always a good option. :-)

Yes, but not always a satisfying one.  :)

>  Where's that StackOverflow item? How many upvotes does it have?


93 - How do I test if int value exists in Python Enum without using try/catch?
     https://stackoverflow.com/q/43634618/208880

25 - How to test if an Enum member with a certain name exists?
     https://stackoverflow.com/q/29795488/208880

  3 - Validate value is in Python Enum values
     https://stackoverflow.com/q/54126570/208880

  2 - How to check if string exists in Enum of strings?
     https://stackoverflow.com/q/63335753/208880

I think I like your constructor change idea, with a small twist:

     Color(value=<sentinel>, name=<sentinel>, default=<sentinal>)

This would make it possible to search for an enum by value or by name, and also specify a default return value (raising an exception if the default is not set and a member cannot be found).

So specifically this would allow (hope my shorthand is clear):
```
Color['RED'] --> Color.RED or raises
Color(1) -> Color.RED or raises
Color(1, default=None) -> Color.RED or None
Color(name='RED', default=None) -> Color.RED or None
```
This seems superficially reasonable. I'm not sure what Color(value=1, name='RED') would do -- insist that both value and name match? Would that have a use case?

My remaining concern is that it's fairly verbose -- assuming we don't really need the name argument, it would be attractive if we could write Color(1, None) instead of Color(1, default=None).

Note that instead of Color(name='RED') we can already write this:
```
getattr(Color, 'RED') -> Color.RED or raises
getattr(Color, 'RED', None) -> Color.RED or None
```

Ricky Teachey

unread,
Mar 15, 2021, 2:36:28 PM3/15/21
to Guido van Rossum, python-ideas
On Mon, Mar 15, 2021 at 2:28 PM Guido van Rossum <gu...@python.org> wrote:

...


I think I like your constructor change idea, with a small twist:

     Color(value=<sentinel>, name=<sentinel>, default=<sentinal>)

This would make it possible to search for an enum by value or by name, and also specify a default return value (raising an exception if the default is not set and a member cannot be found).

So specifically this would allow (hope my shorthand is clear):
```
Color['RED'] --> Color.RED or raises
Color(1) -> Color.RED or raises
Color(1, default=None) -> Color.RED or None
Color(name='RED', default=None) -> Color.RED or None
```

Additional possibility (just raising it; neither for nor against) with PEP 637:

Color['RED', default=None] --> Color.RED or None

Ethan Furman

unread,
Mar 15, 2021, 3:50:43 PM3/15/21
to python-ideas
On 3/15/21 11:27 AM, Guido van Rossum wrote:
> On Mon, Mar 15, 2021 at 10:53 AM Ethan Furman wrote:

>> Part of the reason is that there are really two ways to identify an
>> enum -- by name, and by value -- which should `__contains__` work with?
>
> The two sets don't overlap, so we could allow both. (Funny
> interpretations of `__contains__` are not unusual, e.g.
> substring checks are spelled 'abc' in 'fooabcbar'.)

They could overlap if the Enum is a `str`-subclass -- although having the name of one member match the value of a different member seems odd.

>> I think I like your constructor change idea, with a small twist:
>>
>> Color(value=<sentinel>, name=<sentinel>, default=<sentinal>)
>>
>> This would make it possible to search for an enum by value or by name,
>> and also specify a default return value (raising an exception if the
>> default is not set and a member cannot be found).
>
>
> So specifically this would allow (hope my shorthand is clear):
> ```
> Color['RED'] --> Color.RED or raises
> Color(1) -> Color.RED or raises
> Color(1, default=None) -> Color.RED or None
> Color(name='RED', default=None) -> Color.RED or None
> ```
> This seems superficially reasonable. I'm not sure what
> Color(value=1, name='RED') would do -- insist that both value and
> name match? Would that have a use case?

I would enforce that both match, or raise. Also not sure what the use-case would be.

> My remaining concern is that it's fairly verbose -- assuming we don't
> really need the name argument, it would be attractive if we could
> write Color(1, None) instead of Color(1, default=None).
>
> Note that instead of Color(name='RED') we can already write this:
> ```
> getattr(Color, 'RED') -> Color.RED or raises
> getattr(Color, 'RED', None) -> Color.RED or None

Very good points.

Everything considered, I think I like allowing `__contains__` to verify both names and values, adding `default=<sentinel>` to the constructor for the value-based "gimme an Enum or None" case, and recommending `getattr` for the name-based "gimme an Enum or None" case.

--
~Ethan~
_______________________________________________
Python-ideas mailing list -- python...@python.org
To unsubscribe send an email to python-id...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at https://mail.python.org/archives/list/python...@python.org/message/UQBSDZQJWBKMOVSUES7HEDJTYR76Y5N2/

Guido van Rossum

unread,
Mar 15, 2021, 3:55:48 PM3/15/21
to Ethan Furman, python-ideas
+1

Matt Wozniski

unread,
Mar 16, 2021, 12:41:16 AM3/16/21
to Guido van Rossum, python-ideas
I find the idea of having the constructor potentially return something other than an instance of the class to be very... off-putting. Maybe it's the best option, but my first impression of it isn't favorable, and I can't think of any similar case that exists in the stdlib today off the top of my head. It seems like we should be able to do better.

If I might propose an alternative before this gets set in stone: what if `Enum` provided classmethods `from_value` and `from_name`, each with a `default=<sentinel>`, so that you could do:

Color.from_value(1)  # returns Color.RED
Color.from_value(-1)  # raises ValueError
Color.from_value(-1, None)  # returns None

Color.from_name("RED")  # returns Color.RED
Color.from_name("BLURPLE")  # raises ValueError
Color.from_name("BLURPLE", None)  # returns None

That still allows each concept to be expressed in a single line, and remains explicit about whether the lookup is happening by name or by value. It allows spelling `default=None` as just `None`, as we desire. And instead of being a `__contains__` with unusual semantics coupled with a constructor with unusual semantics, it's a pair of class methods that each have fairly unsurprising semantics.

~Matt

Guido van Rossum

unread,
Mar 16, 2021, 1:35:03 AM3/16/21
to Matt Wozniski, python-ideas
You have a good point (and as static typing proponent I should have thought of that).

Maybe there is not actually a use case for passing an arbitrary default? Then maybe overloading __contains__ (‘in’) might be better? The ergonomics of that seem better for the dominant use case (“is this a valid value for that enum?”).
--
--Guido (mobile)

Marco Sulla

unread,
Mar 16, 2021, 2:37:27 PM3/16/21
to Python Ideas
On Tue, 16 Mar 2021 at 05:38, Matt Wozniski <godl...@gmail.com> wrote:
> Color.from_value(1) # returns Color.RED

What if I have an alias?
_______________________________________________
Python-ideas mailing list -- python...@python.org
To unsubscribe send an email to python-id...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at https://mail.python.org/archives/list/python...@python.org/message/NQ535PUFCWRBBN5QVTGB7QOBNJNJJEPO/

Matt Wozniski

unread,
Mar 16, 2021, 2:47:04 PM3/16/21
to Marco Sulla, Python Ideas
That's a problem with any attempt to find an enum member by value, since values aren't guaranteed to be unique. With either proposal, we'd just need to pick one - probably the one that appears first in the class dict.

Ethan Furman

unread,
Mar 16, 2021, 5:27:06 PM3/16/21
to python...@python.org
On 3/16/21 11:43 AM, Matt Wozniski wrote:
> On Tue, Mar 16, 2021, 2:39 PM Marco Sulla wrote:
>> On Tue, 16 Mar 2021 at 05:38, Matt Wozniski wrote:

>>> Color.from_value(1) # returns Color.RED
>>
>> What if I have an alias?

Aliases are different names for a single Enum member, so a by-value search is unaffected by them.

> That's a problem with any attempt to find an enum member by value,
> since values aren't guaranteed to be unique. With either proposal,
> we'd just need to pick one - probably the one that appears first
> in the class dict.

This is incorrect. Enum values are unique -- there is only one member that will be returned for any given value. Aliases are additional names for that one member:

from enum import Enum

class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
REDD = 1 # to support a silly misspelling

>>> Color.RED
<Color.RED: 1>

>>> Color(1)
<Color.RED: 1>

>>> Color['RED']
<Color.RED: 1>

>>> Color['REDD']
<Color.RED: 1>

Notice that 'REDD' returns the RED member. There is no member named REDD in Color.

--
~Ethan~
_______________________________________________
Python-ideas mailing list -- python...@python.org
To unsubscribe send an email to python-id...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at https://mail.python.org/archives/list/python...@python.org/message/XBGO35VFGC7CQLIZJTKH6EAXEKCJJQRC/

Marco Sulla

unread,
Mar 16, 2021, 9:48:53 PM3/16/21
to Ethan Furman, python-ideas
On Mon, 15 Mar 2021 at 20:49, Ethan Furman <et...@stoneleaf.us> wrote:
> Everything considered, I think I like allowing `__contains__` to verify both names and values

What about Enum.values()?

> adding `default=<sentinel>` to the constructor for the value-based "gimme an Enum or None" case

What's the use case, apart checking if the value is a "member" of the enum?
_______________________________________________
Python-ideas mailing list -- python...@python.org
To unsubscribe send an email to python-id...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at https://mail.python.org/archives/list/python...@python.org/message/QMCYEAAZ3UELOL5G3T2T3G72KVKNAJV3/

Serhiy Storchaka

unread,
Mar 17, 2021, 5:31:28 AM3/17/21
to python...@python.org
12.03.21 23:48, Ethan Furman пише:
> A question that comes up quite a bit on Stackoverflow is how to test to
> see if a value will result in an Enum member, preferably without having
> to go through the whole try/except machinery.
>
> A couple versions ago one could use a containment check:
>
>   if 1 in Color:
>
> but than was removed as Enums are considered containers of members, not
> containers of the member values.  It was also possible to define one's
> own `_missing_` method and have it return None or the value passed in,
> but that has also been locked down to either return a member or raise an
> exception.
>
> At this point I see three options:
>
> 1) add a `get(value, default=None)` to EnumMeta (similar to `dict.get()`
>
> 2) add a recipe to the docs
>
> 3) do nothing
>
> Thoughts?

The Enum class is already too overloaded. I sometimes think about adding
SimpleEnum class with minimal simple functionality which would allow to
use enums in more modules sensitive to import time.

As for solving your problem, try/except looks the best solution to me.

try:
Color(1)
except ValueError:
... # invalid color
else:
... # valid color

If you don't like try/except, the second best solution is to add a
module level helper in the enum module:

def find_by_value(cls, value, default=None):
try:
return cls(value)
except ValueError:
return default

You can add also find_all_by_value(), get_aliases(), etc. It is
important that they are module-level function, so they do not spoil the
namespace of the Enum class.

_______________________________________________
Python-ideas mailing list -- python...@python.org
To unsubscribe send an email to python-id...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at https://mail.python.org/archives/list/python...@python.org/message/PLCJCLTU5FZDHVMJW3BN7FQVD6BPNJBY/

Ricky Teachey

unread,
Mar 17, 2021, 8:35:48 AM3/17/21
to Serhiy Storchaka, python-ideas
On Wed, Mar 17, 2021, 5:34 AM Serhiy Storchaka <stor...@gmail.com> wrote:
12.03.21 23:48, Ethan Furman пише:
> A question that comes up quite a bit on Stackoverflow is how to test to
> see if a value will result in an Enum member, preferably without having
> to go through the whole try/except machinery.
>
...

> Thoughts?

The Enum class is already too overloaded. I sometimes think about adding
SimpleEnum class with minimal simple functionality which would allow to
use enums in more modules sensitive to import time.

As for solving your problem, try/except looks the best solution to me.

    try:
        Color(1)
    except ValueError:
        ... # invalid color
    else:
        ... # valid color

If you don't like try/except, the second best solution is to add a
module level helper in the enum module:

    def find_by_value(cls, value, default=None):
        try:
            return cls(value)
        except ValueError:
            return default

You can add also find_all_by_value(), get_aliases(), etc. It is
important that they are module-level function, so they do not spoil the
namespace of the Enum class.


+1 on a module level helper. 

The preponderance of stackoverflow questions seem convincing enough that people want to do this and it seems like a reasonable request. And I have wanted to do something like this myself, so I'd probably use it. I see it as analogous to situations when you want to use dict.get(key) instead of dict[key].

But adding a non-dunder method to the Enum class namespace seems more suboptimal to me compared to a module level helper, because of the namespace spoiling/crowding issue. No matter what method name were to be chosen, someone at some point would want to use it as an Enum member name (unless of course it's a reserved dunder method).

Reply all
Reply to author
Forward
0 new messages