Skip to content

Make hashes of units a bit more logical, guarding against unlikely cases - #20224

Merged
mhvk merged 2 commits into
astropy:mainfrom
mhvk:units-hashing-safety
Aug 8, 2026
Merged

mhvk merged 2 commits into
astropy:mainfrom
mhvk:units-hashing-safety

Conversation

@mhvk

@mhvk mhvk commented Aug 8, 2026 •

Copy link
Copy Markdown
Contributor

This PR aims to make a bit better effort to make hashes of units logical (and is needed for #20107, where converters are cached).

The basic idea is to let the hash of a composite unit depend on the hashes of the bases, instead of just their names, and
to short-circuit the case of a single base with scale=1 and power=1.

To make this work, the IrredicibleUnit gets a new hash (arguably an improvement in itself).

p.s. This does not aim to address the difference with unit equality and hash equality.

EDIT: I see no point in backporting this.

  • By checking this box, the PR author has requested that maintainers do NOT use the "Squash and Merge" button. Maintainers should respect this when possible; however, the final decision is at the discretion of the maintainer that merges the PR.

@mhvk
mhvk requested review from eerovaher and nstarman August 8, 2026 12:55
@mhvk mhvk added this to the v8.1.0 milestone Aug 8, 2026
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Astropy! 🌌 This checklist is meant to remind the package maintainers who will review this pull request of some common things to look for.

  • Do the proposed changes actually accomplish desired goals?
  • Do the proposed changes follow the Astropy coding guidelines?
  • Are tests added/updated as required? If so, do they follow the Astropy testing guidelines?
  • Are docs added/updated as required? If so, do they follow the Astropy documentation guidelines?
  • Is rebase and/or squash necessary? If so, please provide the author with appropriate instructions. Also see instructions for rebase and squash.
  • Did the CI pass? If no, are the failures related? If you need to run daily and weekly cron jobs as part of the PR, please apply the "Extra CI" label. Codestyle issues can be fixed by the bot.
  • Is a change log needed? If yes, did the change log check pass? If no, add the "no-changelog-entry-needed" label. If this is a manual backport, use the "skip-changelog-checks" label unless special changelog handling is necessary.
  • Is this a big PR that makes a "What's new?" entry worthwhile and if so, is (1) a "what's new" entry included in this PR and (2) the "whatsnew-needed" label applied?
  • At the time of adding the milestone, if the milestone set requires a backport to release branch(es), apply the appropriate "backport-X.Y.x" label(s) before merge.

This lets the hash of a composite unit depend on the
hashes of the bases, instead of just their names, and
that is short-circuits the case of a single base with
scale=1 and power=1.

To make this work, the irredicible unit gets a new hash
(arguably an improvement in itself).
Comment thread astropy/units/core.py
return hash((self.scale, *[x.name for x in self.bases], *map(str, self.powers)))
# if we're just a wrapper around another unit, let our hash be the same
# as that unit's.
if self.scale == 1 and len(self.powers) == 1 and self.powers[0] == 1:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ensures that hash((u.m**2)**.5) continues to equal hash(u.m) (before, both expanded to hash((1.0, "m", "1")).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: I considered changing CompositeUnit.__init__ to __new__ so that it can return the original unit in this case, but it seemed more involved and risky, and more of an API change.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. I think we can make a classmethod for strange constructors, e.g. CompositeUnit.make, but standard constructors should return standard objects (isinstance(CompositeUnit(), CompositeUnit)).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I hadn't thought about the constructor argument; agreed we should only do it for a strong case -- which this is not.

Comment thread astropy/units/core.py
self.__getstate__(),
)

@cached_property

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This used to just inherit, and thus be hash((1.0, self.name, "1")). But that would now lead to infinite recursion. The new choice makes it more similar to NamedUnit, which does hash((self.name, self.represents)). Possibly hash((self.name, None)) is more logical, but it seemed nice to be a bit more unique.

@nstarman

nstarman commented Aug 8, 2026

Copy link
Copy Markdown
Member

@mhvk does changing hashes change any code in a big way, e.g. serializations? Just thinking about the scope of this change. If it's actually far-reaching then we should take a sec to standardize the hash layout so all unit classes can have the same recipe.

E.g. what's the maximum number of slots we need? For CompositeUnit ? Say it's 4, then all units could be hashed as a 4-tuple with a certain pattern.

@mhvk

mhvk commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

@nstarman - in the end, a hash is just an integer, calculate in some way (e.g., xoring the hash of the tuple members, though in practice it is more complicated: https://github.com/python/cpython/blob/998b89020456db591be41e6529b04f4bc8c8181f/Objects/tupleobject.c#L356), and we take pains not to include it in pickle, so I think we're safe on that count.

@nstarman

nstarman commented Aug 8, 2026

Copy link
Copy Markdown
Member

we take pains not to include it in pickle, so I think we're safe on that count

Good. Then I agree that I think we're relatively free to iterate on the hashes even within dot releases.

@nstarman

nstarman commented Aug 8, 2026

Copy link
Copy Markdown
Member

@mhvk

mhvk commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

Those are unrelated, thankfully: #20164 and the top_k in the works (Quantity done in #20191; Masked` still to be added)

@mhvk

mhvk commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

OK, I'll be a bit blunt and merge so I can rebase #20107 and open that back up for review; @eerovaher - I'll be happy to do a follow-up if you have suggestions.

@mhvk
mhvk merged commit c515467 into astropy:main Aug 8, 2026
44 of 46 checks passed
@mhvk
mhvk deleted the units-hashing-safety branch August 8, 2026 15:49
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.

2 participants