Python Patterns: @total_ordering

A drawing of a red, black, and yellow milk snake from Biologia Centrali Americana.

Python classes come with a set of rich comparison operators. I can compare strings lexically like so:

"alex" > "alan"
"cat" < "dog"

And I can sort numbers including integers and floats:

sorted((4, 3, 2.2, 5)) == [2.2, 3, 4, 5]

All of these are made possible by special methods defined by each class. Implementing comparison and sorting for your own classes means defining six methods, one each for ==, !=, >, =>, <, and <=. Thankfully, Python has a helper method that makes it even simpler: the @total_ordering decorator.

Your Library

Let’s make a class to hold books so we can keep track of our library. A basic Book class might look like this:

class Book:
  def __init__(self, title, author):
    self.title = title
    self.author = author

We want the Book class to be comparable because that will allow us to order the books on the shelf (using sorted() for instance). Books will be sorted first by author and then by title. To implement that, we might write the six special methods like this:

class Book:
  def __init__(self, title, author):
    self.title = title
    self.author = author

  # Define ==
  def __eq__(self, other):
    ours = (self.author, self.title)
    theirs = (other.author, other.title)
    return ours == theirs

  # Define !=
  def __ne__(self, other):
    ours = (self.author, self.title)
    theirs = (other.author, other.title)
    return ours != theirs

  # Define <
  def __lt__(self, other):
    ours = (self.author, self.title)
    theirs = (other.author, other.title)
    return ours < theirs

  # Define <=
  def __le__(self, other):
    ours = (self.author, self.title)
    theirs = (other.author, other.title)
    return ours <= theirs

  # Define >
  def __gt__(self, other):
    ours = (self.author, self.title)
    theirs = (other.author, other.title)
    return ours > theirs

  # Define >=
  def __ge__(self, other):
    ours = (self.author, self.title)
    theirs = (other.author, other.title)
    return ours >= theirs

That is a lot of boilerplate code!

Math tells us that if self > other is true, than self < other and self == other are false. We could write our own logic taking advantage of this fact to reduce the boilerplate, but that is exactly what @total_ordering from functools does already!

With @total_ordering

Using the @total_ordering decorator1 we only have to define __eq__ and one of the other comparison methods. The rest of the methods are filled in for us. It’s used like so:

from functools import total_ordering


@total_ordering
class Book:
  def __init__(self, title, author):
    self.title = title
    self.author = author

  # Define ==
  def __eq__(self, other):
    ours = (self.author, self.title)
    theirs = (other.author, other.title)
    return ours == theirs

  # Define <
  def __lt__(self, other):
    ours = (self.author, self.title)
    theirs = (other.author, other.title)
    return ours < theirs

Now we have a much more compact class, but with the same functionality as before! We can sort our books easily:

my_books = [
  Book("Absalom, Absalom!", "William Faulkner"),
  Book("The Sun Also Rises", "Ernest Hemingway"),
  Book("For Whom The Bell Tolls", "Ernest Hemingway"),
  Book("The Sound and the Fury", "William Faulkner"),
]

for book in sorted(my_books):
  output = "{author}, {title}".format(
    author=book.author,
    title=book.title,
  )
  print(output)

# >> Ernest Hemingway, For Whom The Bell Tolls
# >> Ernest Hemingway, The Sun Also Rises
# >> William Faulkner, Absalom, Absalom!
# >> William Faulkner, The Sound and the Fury

And we didn’t have to write six methods!


  1. A decorator is a function that takes a Python object as an argument and returns a (often) modified copy of the object.