Skip to content

gh-158856: Avoid a linear scan per member when creating an Enum - #158857

Open
jonbaldie wants to merge 1 commit into
python:mainfrom
jonbaldie:perf-enum-hashable-values
Open

jonbaldie wants to merge 1 commit into
python:mainfrom
jonbaldie:perf-enum-hashable-values

Conversation

@jonbaldie

@jonbaldie jonbaldie commented Oct 5, 2026 •

Copy link
Copy Markdown

Summary

Every time a member is added, _proto_member.__set_name__ runs value not in enum_class._hashable_values_. That's a list, so each new member scans every member before it, and building the class is O(n²).

The scan is only needed when the value was already in _value2member_map_, which happens for aliases and Flag pseudo-members. If setdefault just added the key, the value can't be in the list yet, because the list only ever gets values that were added to the map, and nothing removes from the map. So I compare the map's size before and after and only scan when it didn't grow:

-enum_class._value2member_map_.setdefault(value, enum_member)
-if value not in enum_class._hashable_values_:
+size = len(value2member_map)
+value2member_map.setdefault(value, enum_member)
+if (len(value2member_map) != size
+        or value not in enum_class._hashable_values_):
     enum_class._hashable_values_.append(value)

It's the same symptom as gh-89580, which was fixed back then; this scan was added later.

Evidence

Enum('Generated', {f'M{i:08x}': i for i in range(n)}), 3.16 dev build, macOS arm64, median of 5, two alternating runs:

n before after
1,000 5.8 ms 3.1 ms
4,000 54 ms 12.5 ms
8,000 188 ms 25 ms
16,000 713 ms 53 ms
small IntEnum (6 members, 1 alias) 40.2 µs 38.8 µs (noise)

New test test_hashable_values_creation_work counts __eq__ calls while building a 200-member enum:

  • Before: AssertionError: 19900 not less than 800
  • After: passes

test_hashable_values_with_aliases checks that aliases still don't add duplicates to _hashable_values_.

_hashable_values_ is identical before and after for all 70 enum classes created when importing ssl, signal, socket, re, http, inspect, uuid, ast and a few more. test_enum: 1,093 run, all pass. test_signal, test_http_cookies, test_re, test_ssl, test_inspect, test_pydoc and test_socket show the same 6 failures with and without the patch; they're local-environment failures in the CLI and online-docs tests.

Note: Enum('X', ['A', 'B', ...]) (a list of names, so auto values) is still quadratic, because _create_ copies last_values and _generate_next_value_ sorts it for every member. That's a public hook that people override, so I've left it alone here.

Merge Danger

Door: two-way

Three lines in one private code path. _hashable_values_ ends up with the same contents in the same order.

Blast Radius: stdlib

Every Enum class goes through this code. The only way behaviour could change is a value whose __eq__ and __hash__ disagree, which already breaks _value2member_map_ lookups.

AI disclosure: the profiling, the patch and the equivalence check were done with AI tools (Codex, Claude).

🤖 Generated with Claude Code

`_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 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant