Skip to content

Return NotImplemented from comparisons with non-Ksuid objects#62

Merged
svix-jbrown merged 3 commits into
svix:mainfrom
gaoflow:fix-comparison-non-ksuid
Jun 17, 2026
Merged

Return NotImplemented from comparisons with non-Ksuid objects#62
svix-jbrown merged 3 commits into
svix:mainfrom
gaoflow:fix-comparison-non-ksuid

Conversation

@gaoflow

@gaoflow gaoflow commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Problem

Ksuid.__eq__ asserts isinstance(other, self.__class__), so comparing a Ksuid against any other type raises AssertionError instead of returning False:

>>> from ksuid import Ksuid, KsuidMs
>>> k = Ksuid()
>>> k == None
AssertionError
>>> None in [k]
AssertionError
>>> KsuidMs() == Ksuid()
AssertionError

This breaks ordinary operations — ksuid == None, ksuid in some_list (list membership uses ==), comparing a Ksuid with a KsuidMs, and dict/set use with mixed types. It is also fragile under python -O, where assert is stripped and the same comparison instead raises AttributeError ('NoneType' object has no attribute '_uid'). __lt__ has the same problem.

Fix

Per the Python data model, rich comparison methods must return NotImplemented (never raise) when the other operand is of an unsupported type. __eq__ and __lt__ now return NotImplemented when other is not a Ksuid, so equality falls back to False and ordering raises a proper TypeError. Comparison between Ksuid and KsuidMs (both 20-byte values) now compares by raw bytes, consistent with __hash__.

Tests

Added test_eq_with_non_ksuid_does_not_raise, test_eq_by_raw_bytes, and test_ordering_with_non_ksuid_raises_type_error. The full suite passes (2021), and ty/ruff check/ruff format --check are clean on the ksuid package.

I worked on this with AI assistance under my direction and reviewed the change myself.

@gaoflow gaoflow requested a review from a team as a code owner June 15, 2026 23:53
@svix-jbrown

Copy link
Copy Markdown
Contributor

good catch, but I'm not sure I entirely agree with the logic for cross-class comparisons; I kind of feel like KsuidMs should only be equal to another KsuidMs, not to a Ksuid that happens to have the same internal byte representation.

def __eq__(self, other: object) -> bool:
    if not isinstance(other, Ksuid) or self.TIMESTAMP_LENGTH_IN_BYTES != other.TIMESTAMP_LENGTH_IN_BYTES:
        return NotImplemented
    return self._uid == other._uid

Along with a test confirming that a carefully-constructed Ksuid and KsuidMs can't overlap?

@gaoflow

gaoflow commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

Good point, and agreed. I've updated __eq__ to guard on TIMESTAMP_LENGTH_IN_BYTES exactly as you suggested, so a Ksuid and a KsuidMs never compare equal even when their 20 raw bytes coincide.

Added test_eq_across_ksuid_subclasses_is_false, which constructs both types from the same bytes(range(20)), asserts their raw bytes are identical, and asserts they are not equal in either direction (while same-class equality still holds). Full suite is green.

@svix-jbrown

Copy link
Copy Markdown
Contributor

Please also run uv run ruff format ksuid and commit the results.

@svix-jbrown svix-jbrown left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sounds good to me! Thanks for your contribution!

gaoflow added 3 commits June 17, 2026 14:49
Ksuid.__eq__ asserted isinstance(other, self.__class__), so comparing a
Ksuid with any other type raised AssertionError instead of returning
False. This broke ordinary operations such as 'ksuid == None',
'ksuid in some_list', and comparing a Ksuid with a KsuidMs, and the
assert is stripped under 'python -O' (turning the error into an
AttributeError). __lt__ had the same problem.

Follow the data model and return NotImplemented when the other operand
is not a Ksuid, so equality falls back to False and ordering raises a
proper TypeError.
A Ksuid and a KsuidMs are both 20 bytes and can share a byte
representation, but they decode their timestamps differently. Guard
__eq__ on TIMESTAMP_LENGTH_IN_BYTES so the two subclasses never compare
equal just because their raw bytes coincide, and add a test that
constructs both from the same bytes and asserts inequality.
@svix-jbrown svix-jbrown force-pushed the fix-comparison-non-ksuid branch from 57a032c to 7f8f53e Compare June 17, 2026 21:49
@svix-jbrown svix-jbrown enabled auto-merge (squash) June 17, 2026 21:49
@svix-jbrown svix-jbrown merged commit 4e274da into svix:main Jun 17, 2026
12 checks passed
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