Skip to content

perf: cache __hash__ on Specifier, Marker, and Requirement - #1252

Draft
henryiii wants to merge 1 commit into
pypa:mainfrom
henryiii:perf/cached-hashes
Draft

perf: cache __hash__ on Specifier, Marker, and Requirement#1252
henryiii wants to merge 1 commit into
pypa:mainfrom
henryiii:perf/cached-hashes

Conversation

@henryiii

@henryiii henryiii commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

In draft, I think we should work on making these immutable first.

🤖 AI text below 🤖

Specifier.__hash__ recomputed _canonical_spec (a parse + canonicalization) on every call, and Marker.__hash__/Requirement.__hash__ re-serialized their whole tree each time. All three are immutable, so this caches the hash lazily using the same _hash_cache pattern Version adopted in #1118: the slot/attribute is initialized to None on every construction path (including __setstate__, Marker._from_markers, and the Marker.__new__ bypass inside Requirement.__init__), and __getstate__ already returns explicit state so the cache never leaks into pickles. Hash values and equality semantics are unchanged.

timeit (Python 3.14, Apple M5 Pro; the asv suite has no hash benchmarks — the full suite shows no regressions):

Type main this PR
hash(Specifier) 552 ns 38 ns
hash(Marker) 936 ns 37 ns
hash(Requirement) 1068 ns 40 ns

SpecifierSet is deliberately untouched here; its canonicalization interacts with other caches and is handled in #1253.

Part of #1239.

🤖 Generated with Claude Code

Specifier.__hash__ recomputed _canonical_spec (parsing and canonicalizing
the version) on every call, Marker.__hash__ re-serialized its whole marker
tree, and Requirement.__hash__ re-serialized all of its parts. All three
types are immutable, so cache the computed hash lazily in a _hash_cache
attribute, mirroring the pattern Version already uses.

For the slotted classes (Specifier, Marker) a _hash_cache slot is added and
initialized to None on every construction path, including alternate
constructors (Marker._from_markers, the Marker built directly in
Requirement.__init__) and the __setstate__ pickle paths. The existing
explicit __getstate__ implementations return only the core fields, so the
cache is never serialized into new pickles. Requirement has no __slots__, so
a plain attribute is used and initialized in __init__ and in both
__setstate__ branches.

Assisted-by: ClaudeCode:claude-opus-4-8
@henryiii
henryiii marked this pull request as ready for review June 12, 2026 19:07
@notatallshaw

notatallshaw commented Jun 13, 2026

Copy link
Copy Markdown
Member

Requirement doesn't indicate that it should be immutable, it doesn't have __slots__ and all the attributes are public and writable to.

I would be up for deprecating it's mutability and/or making all it's attributes properties that clear cache on writing to, but I don't think we can assume people aren't mutating it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants