Comparisons with total_ordering

functools · Practical · 8 min

Custom types that sort, compare or go into heaps need all six comparison methods to agree.

For objects to work with sorted, min, max, bisect and heapq, they need comparison methods. Python has six: __eq__, __ne__, __lt__, __le__, __gt__, __ge__. __ne__ is derived from __eq__ automatically, but the four ordering methods are not.

@total_ordering is a class decorator: define __eq__ and one of __lt__, __le__, __gt__ or __ge__, and it supplies the other three.

@total_ordering
class Version:
    def __eq__(self, other): ...
    def __lt__(self, other): ...

Implementation tips:

  • Return NotImplemented (not False) when other is an unsupported type. Python then tries the reflected operation and finally raises a clean TypeError.
  • Compare tuples of fields: (self.major, self.minor) < (other.major, other.minor) handles lexicographic order for you.
  • If you define __eq__, define __hash__ too if instances should be hashable (otherwise they become unhashable).

The generated methods add a small overhead per comparison; for hot sorting paths, define all methods yourself or use a key= function. For simple records, @dataclass(order=True) generates all comparisons from the fields in order, which is often easier still.

Example

from functools import total_ordering

@total_ordering
class Version:
    def __init__(self, text):
        self.parts = tuple(int(p) for p in text.split("."))
    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
    def __hash__(self):
        return hash(self.parts)
    def __repr__(self):
        return "v" + ".".join(map(str, self.parts))

vs = [Version("1.10.0"), Version("1.2.3"), Version("1.2")]
print(sorted(vs))                              # [v1.2, v1.2.3, v1.10.0]
print(Version("2.0") >= Version("1.9"))        # True  (generated)
print(max(vs), Version("1.2") in {Version("1.2")})   # v1.10.0 True

Remember

  • Define __eq__ plus one ordering method; @total_ordering adds the rest
  • Return NotImplemented for unsupported types
  • Consider @dataclass(order=True) or a key function instead

Watch out for

  • Returning False instead of NotImplemented for foreign types, which hides type errors.

Check yourself

  1. With @total_ordering, which methods must you define yourself?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/functools.html#functools.total_ordering

Builds on

Unlocks

Nothing builds on this yet.