5

I am writing several functions that handle ordered datasets. Sometime, there is an argument that can be a int or float or a timestamp or anything that supports comparison (larger than / smaller than) and that I can use for trimming data for instance.

Is there a way to type-hint such a parameter? The typing module doesn't seem to include this, but is there some other way?

2
  • There's really no such thing as broad as "supports comparison". int values, for example, can be compared to other numbers, but not strings. Commented Aug 26, 2022 at 16:59
  • I would do something like ComparableMixin (stackoverflow.com/questions/6907323/…) and check for objects that inherit from it. But as @chepner wrote, I'm afraid this is not quite generic as in other languages e.g. C# Commented Aug 26, 2022 at 17:00

1 Answer 1

6

There is no standard 'comparable' ABC, no, as the rich comparison methods are really very flexible and don't necessarily return booleans.

The default built-in types return NotImplemented when applied to a type they can't be compared with, for example, while specialised libraries like SQLAlchemy and numpy use rich comparison methods to return completely different objects. See the documentation for the rich comparison methods for the details.

But you should be able to define a a Protocol subclass for specific expectations:

from typing import Protocol, TypeVar

T = TypeVar("T", infer_variance=True)

class Comparable(Protocol[T]):
    def __lt__(self: T, other: T) -> bool:
        ...

    # ... etc

You may need to tweak the protocol to fit your exact expectations, and / or use a non-generic version that's specific to the types you use (perhaps with @overloaded definitions for specific types).

For sorting with the builtin sorted() function, just having a __lt__ method should suffice that accepts any value to compare with.

The typeshed project defines a SupportsRichComparison type alias and SupportsRichComparisonT typevar bound to the type alias, which is defined as the union of two such protocols:

class SupportsDunderLT(Protocol[_T_contra]):
    def __lt__(self, other: _T_contra, /) -> bool: ...

class SupportsDunderGT(Protocol[_T_contra]):
    def __gt__(self, other: _T_contra, /) -> bool: ...

SupportsRichComparison: TypeAlias = SupportsDunderLT[Any] | SupportsDunderGT[Any]
SupportsRichComparisonT = TypeVar("SupportsRichComparisonT", bound=SupportsRichComparison)

The built-in sorted() function is annotated as accepting Iterable[SupportsRichComparisonT] as its first argument, returning list[SupportsRichComparisonT].

You can import these from the _typeshed package, provided you import them behind a TYPE_CHECKING guard. This works because type checkers include the typeshed types by default when checking:

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from _typeshed import SupportsRichComparisonT
Sign up to request clarification or add additional context in comments.

4 Comments

but since every class inherits __eq__, __lt__ etc. from object, doesn't every object per default adhere to this protocol? At least annotating e.g. my_fun(a: Comparable, b: Comparable) and passing instances of, say class Test: ... (which doesn't override __eq__, __lt__, etc.) doesn't throw off MyPy.
@Darkdragon84: I can't reproduce this with pyright: Code sample in pyright playground
@Darkdragon84: Note that object does not implement __lt__, __gt__, __le__ or __ge__, only __eq__ and __ne__; e.g. object() < object() raises TypeError: '<' not supported between instances of 'object' and 'object'.
ah yes, good point. I just came across this in the context of __hash__ and __eq__ consistency too.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.