skip to content

Which methods must a Python class define so that < and sorted() work on its instances?

level: middleimportance: must knowfreq 55%

answer

  1. Five operators, five separate methods
  2. Ordering is not inherited from object
  3. The sort machinery asks only one question
  4. One method plus equality, decorator fills the rest
  5. functools.total_ordering trades speed for brevity

basics

~20 s

Define lt: sorted(), min() and max() need nothing else, because they only ever ask whether one element is less than another. For the full set of ordering operators, add eq and decorate the class with functools.total_ordering, or write all four yourself.

solid answer

~40 s

Every comparison operator maps to a dunder method: `<` to `__lt__`, `<=` to `__le__`, `>` to `__gt__`, `>=` to `__ge__`, `==` to `__eq__`. `object` supplies `__eq__` (identity) but no ordering, so `<` between two plain instances raises `TypeError`. Python's sort machinery uses **only** `__lt__`, so a single method makes a class sortable. If you want all four operators, either write them or apply `functools.total_ordering`, which fills in the missing three from one you defined plus `__eq__`; the derived operators go through Python-level wrappers, so they are slower than hand-written ones. `@dataclass(order=True)` generates all four, comparing the tuple of fields in declaration order and honouring `field(compare=False)`. `__ne__` you never write — Python 3 derives it from `__eq__` automatically.

code

python · 21 lines
python
import functools


@functools.total_ordering
class Version:
    def __init__(self, major, minor):
        self.parts = (major, minor)

    def __eq__(self, other):
        if not isinstance(other, Version):
            return NotImplemented
        return self.parts == other.parts

    def __lt__(self, other):
        if not isinstance(other, Version):
            return NotImplemented
        return self.parts < other.parts


print(Version(3, 14) > Version(3, 9))
print(Version(3, 9) <= Version(3, 9))

go deeper

for a junior

Know that comparison operators are backed by methods, and that a plain class cannot be sorted until you give it one. Recognise lt as the method behind the less-than operator.

for a middle

Explain that the sort machinery only calls lt, show total_ordering deriving the rest from one method plus eq, and mention dataclass order=True comparing fields in declaration order. Delegating to a tuple of fields should be your reflex.

for a senior

Show judgment about consistency and cost: operators derived from the same field tuple, total_ordering's per-call overhead in hot paths, and the decision to leave a type unordered when it has several plausible orderings.

for a principal

Own it as API design. An intrinsic order is a promise every downstream sort, heap and bisect relies on; changing it later silently changes results across a codebase. Decide deliberately whether the order lives on the type or at the call site, and document which.

## The operator-to-method mapping Python's comparison operators are ordinary method calls on the operands: | operator | method | reflected method | |---|---|---| | `a < b` | `a.__lt__(b)` | `b.__gt__(a)` | | `a <= b` | `a.__le__(b)` | `b.__ge__(a)` | | `a > b` | `a.__gt__(b)` | `b.__lt__(a)` | | `a >= b` | `a.__ge__(b)` | `b.__le__(a)` | | `a == b` | `a.__eq__(b)` | `b.__eq__(a)` | These five are the *rich comparison* methods, and they are independent: implementing `__lt__` gives you `<` and, by reflection, `>` against instances of the same class — but not `<=` or `>=`. `object` provides a default `__eq__` based on identity and no ordering methods at all, which is why `SomeClass() < SomeClass()` raises `TypeError: '<' not supported between instances of ...`. Nothing about a general object implies an order, so Python refuses to invent one. ## Sorting needs only `__lt__` `list.sort()`, `sorted()`, `min()`, `max()`, `heapq` and `bisect` all express their work in terms of a single question: is this one less than that one? Define `__lt__` and your instances are sortable. This is the cheapest way to make a domain type usable, and it is often all a type needs. ```python class Version: def __init__(self, major, minor): self.parts = (major, minor) def __lt__(self, other): if not isinstance(other, Version): return NotImplemented return self.parts < other.parts ``` Delegating to a tuple of the fields is the idiomatic body: tuples already compare lexicographically, so you get sensible multi-field ordering for free. ## `functools.total_ordering` Writing four near-identical methods is noise. `functools.total_ordering` is a class decorator: given `__eq__` and **one** of `__lt__`, `__le__`, `__gt__`, `__ge__`, it supplies the other three by composing what you gave it. It only fills in methods the class does not already define, so you can override any of them and keep the decorator. Two caveats worth stating in an interview. First, the derived operators are Python-level functions that call your method and then combine the result, so they are slower than hand-written ones — in a hot comparison path (a huge sort, a heap under load) writing the four out, or sorting with a `key=` instead, is measurably faster. Second, it needs `__eq__`: without it the derived `__le__` and `__ge__` cannot be expressed. ## Dataclasses `@dataclass(order=True)` generates all four ordering methods, each comparing `(self.field1, self.field2, ...)` against the other object's tuple in **declaration order**, and returning `NotImplemented` when the other operand is not the same class. `field(compare=False)` excludes a field from both `__eq__` and the ordering, which is how you keep a display name or a cache out of the comparison. If the natural order is not declaration order, add a dedicated sort field, or drop `order=True` and write `__lt__`. ## Consistency is your responsibility Python enforces no relationship between the operators. If `__eq__` says two objects are equal, nothing stops your `__lt__` from also saying one is less — and then sorting, `bisect` and `heapq` produce nonsense that is very hard to trace. Keep the operators consistent with each other: derive them all from the same tuple of fields, which is exactly what `total_ordering` and the dataclass machinery assume. A related discipline: a comparison method should be **total and stable for the values it accepts** — no randomness, no reading the clock, no depending on mutable state that the sort itself might change. ## Intrinsic order versus a sort key The real design question is whether the order belongs to the type at all. A version number, a money amount, a timestamped record — these have one obvious order, and putting it on the type spares every call site. A user record has many plausible orders (by surname, by signup date, by activity), and picking one to be `__lt__` is a decision later readers will fight. There, give callers `key=` functions (or `operator.attrgetter`) and leave the type unordered. Types with several equally valid orderings should not pretend to have one.

  • What does functools.total_ordering cost you compared with writing all four methods?
    The three derived operators are Python-level wrappers that call the method you wrote and combine its result, so each comparison pays an extra call. For ordinary code that is irrelevant; inside a large sort or a heap that is hammered in a tight loop it shows up in profiles, and either hand-written methods or a `key=` function is faster.
  • Your class defines __lt__ but comparing an instance with an int raises TypeError. Is that a bug?
    Usually not — it is the intended outcome. Your `__lt__` should return `NotImplemented` for a type it does not understand; Python then tries the reflected method on the other operand and, when that also declines, raises `TypeError`. That is far better than inventing an order between unrelated types and sorting silently wrong.
  • When should a type not define ordering at all?
    When it has several equally reasonable orders. Choosing one to be `__lt__` makes the other call sites read misleadingly and invites bugs when someone sorts and gets an order they did not expect. Expose the orderings as `key=` functions or `operator.attrgetter` at the call site and leave the operators undefined.

saying these in an interview costs you the question

  • Thinks every object is orderable by default
  • Believes sorted() needs all four ordering methods
  • Writes __ne__ by hand alongside __eq__
  • Uses total_ordering without defining __eq__
  • Defines __lt__ inconsistently with __eq__
  • Gives a type an arbitrary order it does not really have

context