Skip to content

Iterating over enum.Flag types ignores zero-valued in Python 3.11 #109633

Description

@gmgunter

Bug report

Bug description:

In Python 3.10, you can iterate over enum.Enum and enum.Flag types to yield their values.

>>> import sys; print(sys.version)
3.10.12 | packaged by conda-forge | (main, Jun 23 2023, 22:40:32) [GCC 12.3.0]

>>> import enum
>>> class MyEnum(enum.Enum):
...     A = 0
...     B = 1
...     C = 2
...
>>> list(MyEnum)
[<MyEnum.A: 0>, <MyEnum.B: 1>, <MyEnum.C: 2>]

>>> class MyFlag(enum.Flag):
...     A = 0
...     B = 1
...     C = 2
...
>>> list(MyFlag)
[<MyFlag.A: 0>, <MyFlag.B: 1>, <MyFlag.C: 2>]

In Python 3.11, the same is true for enum.Enum types, but the behavior seems to have changed for enum.Flag types. Now, iterating over the type only yields its nonzero values.

>>> import sys; print(sys.version)
3.11.5 | packaged by conda-forge | (main, Aug 27 2023, 03:34:09) [GCC 12.3.0]

>>> import enum
>>> class MyEnum(enum.Enum):
...     A = 0
...     B = 1
...     C = 2
...
>>> list(MyEnum)
[<MyEnum.A: 0>, <MyEnum.B: 1>, <MyEnum.C: 2>]

>>> class MyFlag(enum.Flag):
...     A = 0
...     B = 1
...     C = 2
...
>>> list(MyFlag)
[<MyFlag.B: 1>, <MyFlag.C: 2>]  # Missing <MyFlag.A: 0> (!)

CPython versions tested on:

3.10, 3.11

Operating systems tested on:

Linux

Activity

  1. CharlieZhao95 commented on Sep 21, 2023

    @CharlieZhao95
    Contributor

    I bisected to here:

    commit 7aaeb2a
    Author: Ethan Furman ethan@stoneleaf.us
    Date: Mon Jan 25 14:26:19 2021 -0800
    bpo-38250: [Enum] single-bit flags are canonical (GH-24215)

  2. sobolevn commented on Sep 21, 2023

    @sobolevn
    Member

    From the docs, I can say that now 0 is treated as more like a special value:
    https://docs.python.org/3/library/enum.html#enum.Flag

    Check how <Color: 0> is used all over the place.

    The technical reason behind this is that now 0 does not get into _member_names_ here:

    cpython/Lib/enum.py

    Lines 313 to 319 in 712cb17

    elif (
    Flag is not None
    and issubclass(enum_class, Flag)
    and _is_single_bit(value)
    ):
    # no other instances found, record this member in _member_names_
    enum_class._member_names_.append(member_name)

    Because 0 is not a _is_single_bit member (it is explicit):

    cpython/Lib/enum.py

    Lines 93 to 98 in 712cb17

    def _is_single_bit(num):
    """
    True if only one bit set in num (should be an int)
    """
    if num == 0:
    return False

    So, when we use __iter__ over _member_names_ zero-value is not there:

    cpython/Lib/enum.py

    Lines 790 to 794 in 712cb17

    def __iter__(cls):
    """
    Return members in definition order.
    """
    return (cls._member_map_[name] for name in cls._member_names_)

    We need @ethanfurman :)

  3. self-assigned this
    on Sep 21, 2023
  4. ethanfurman commented on Sep 21, 2023

    @ethanfurman
    Member

    It is more accurate to say that Enum and Flag iterate over the canonical values. For example:

    class Color(Enum):
        RED = 1
        GREEN = 2
        BLUE = 3
        ROJO = 1
        VERDE = 2
        AZUL = 3
    
    list(Color)
    # RED, GREEN, BLUE
    
    class BlendColor(Flag):
        BLACK = 0
        RED = 1
        GREEN = 2
        BLUE = 4
        WHITE = RED | GREEN | BLUE
    
    list(BlendColor)
    # RED, GREEN, BLUE
    

    Aliases have always been omitted from iteration, and flag values with no, or more than one, bit(s) set are considered aliases since Python 3.11 -- this was considered a bug-fix.

  5. gmgunter commented on Sep 22, 2023

    @gmgunter
    Author

    Oh I see. Thanks for the clarification, @ethanfurman.

    Looking back over the docs now, they do pretty much convey everything that you've explained here, although some of the details are subtle. The only thing that I think isn't explicitly explained there is that zero-valued flags are considered aliases.

  6. wimglenn commented on Jun 27, 2024

    @wimglenn
    Contributor
    >>> class F(enum.Flag):
    ...     f = 0
    ... 
    >>> len(F)
    0
    >>> list(F)
    []
    

    Why isn't F.f considered to be canonical?

  7. ethanfurman commented on Jun 27, 2024

    @ethanfurman
    Member

    Because f is not a flag, it is a name for the absence of flags.

  8. wimglenn commented on Jun 27, 2024

    @wimglenn
    Contributor

    f shows up in F.__members__, but not in dir(F). Is that intended?

  9. ethanfurman commented on Jun 28, 2024

    @ethanfurman
    Member

    Yes. dir(F) doesn't list aliases, but __members__ does (which also prevents aliases from being overridden -- i.e. F.f = 99 raises an AttributeError).

  10. ClundXIII commented on Dec 5, 2024

    @ClundXIII

    Hi,

    I do have a use case where I need both alias and elements of a flag. Is there a way to explicitly iterate over either only elements and only aliases that works in both old AND new version?

  11. Leo1690 commented on Mar 21, 2025

    @Leo1690

    @ClundXIII You can do this

    from enum import IntFlag
    
    class Bla(IntFlag):
        X = 0
    
    list(Bla.__members__.values())
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

type-bugAn unexpected behavior, bug, or error

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions