From fb2d736136c324f80975ac4c4e42570e9080427c Mon Sep 17 00:00:00 2001 From: Jonathan Baldie Date: Mon, 5 Oct 2026 15:15:03 +0100 Subject: [PATCH] gh-158856: Avoid a linear scan per member when creating an Enum `_hashable_values_` is a list, so checking it for each new member made class creation quadratic. Skip the check when the value was just added to `_value2member_map_`, since it can't be in the list yet. Co-Authored-By: Claude Opus 5.5 --- Lib/enum.py | 8 ++++-- Lib/test/test_enum.py | 28 +++++++++++++++++++ ...-10-05-15-20-00.gh-issue-158856.Qe7nVb.rst | 2 ++ 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-05-15-20-00.gh-issue-158856.Qe7nVb.rst diff --git a/Lib/enum.py b/Lib/enum.py index 076aa18a02fd20a..432a7f79ac9afeb 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -316,8 +316,12 @@ def __set_name__(self, enum_class, member_name): # This may fail if value is not hashable. We can't add the value # to the map, and by-value lookups for this value will be # linear. - enum_class._value2member_map_.setdefault(value, enum_member) - if value not in enum_class._hashable_values_: + value2member_map = enum_class._value2member_map_ + size = len(value2member_map) + value2member_map.setdefault(value, enum_member) + # A new key can't already be in the list, so skip the linear scan. + if (len(value2member_map) != size + or value not in enum_class._hashable_values_): enum_class._hashable_values_.append(value) except TypeError: # keep track of the value in a list so containment checks are quick diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 447f847f33da938..7570e0c3db31aa0 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -3530,6 +3530,34 @@ class Directions(Enum): UNRESTRICTED = frozenset({"sc", "cs"}) self.assertIs(Directions({"sc"}), Directions.DOWN_ONLY) + def test_hashable_values_creation_work(self): + class CountedInt(int): + comparisons = 0 + __hash__ = int.__hash__ + + def __eq__(self, other): + type(self).comparisons += 1 + return super().__eq__(other) + + size = 200 + values = [CountedInt(i) for i in range(size)] + CountedInt.comparisons = 0 + Big = Enum('Big', [(f'M{i}', v) for i, v in enumerate(values)]) + self.assertLess(CountedInt.comparisons, 4 * size) + self.assertEqual(len(Big), size) + self.assertEqual(Big._hashable_values_, values) + self.assertIn(values[-1], Big) + + def test_hashable_values_with_aliases(self): + class Color(Enum): + RED = 1 + GREEN = 2 + CRIMSON = 1 + BLUE = 3 + self.assertEqual(Color._hashable_values_, [1, 2, 3]) + self.assertIs(Color.CRIMSON, Color.RED) + self.assertIn(1, Color) + class TestOrder(unittest.TestCase): "test usage of the `_order_` attribute" diff --git a/Misc/NEWS.d/next/Library/2026-10-05-15-20-00.gh-issue-158856.Qe7nVb.rst b/Misc/NEWS.d/next/Library/2026-10-05-15-20-00.gh-issue-158856.Qe7nVb.rst new file mode 100644 index 000000000000000..f16f5141bcec347 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-05-15-20-00.gh-issue-158856.Qe7nVb.rst @@ -0,0 +1,2 @@ +Speed up creation of :class:`~enum.Enum` classes with many members: adding a +member no longer scans the list of every value added before it.