functools.total_ordering¶
The @total_ordering class decorator auto-generates missing comparison methods. Define __eq__ and one of __lt__, __le__, __gt__, or __gt__, and total_ordering fills in the remaining four.
from functools import total_ordering
The Problem¶
Python's comparison operators each map to a separate dunder method:
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def __eq__(self, other):
return self.celsius == other.celsius
def __lt__(self, other):
return self.celsius < other.celsius
def __le__(self, other):
return self.celsius <= other.celsius
def __gt__(self, other):
return self.celsius > other.celsius
def __ge__(self, other):
return self.celsius >= other.celsius
# 5 methods for 6 operators (__ne__ auto-derives from __eq__)
# This is tedious and error-prone
The Solution¶
from functools import total_ordering
@total_ordering
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def __eq__(self, other):
return self.celsius == other.celsius
def __lt__(self, other):
return self.celsius < other.celsius
# All six comparisons now work
t1 = Temperature(20)
t2 = Temperature(25)
print(t1 < t2) # True (defined)
print(t1 <= t2) # True (auto-generated)
print(t1 > t2) # False (auto-generated)
print(t1 >= t2) # False (auto-generated)
print(t1 == t2) # False (defined)
print(t1 != t2) # True (auto from __eq__)
Requirements¶
You must define exactly:
__eq__— always required- One of:
__lt__,__le__,__gt__,__ge__
from functools import total_ordering
# Any one of these works with __eq__:
@total_ordering
class A:
def __eq__(self, other): ...
def __lt__(self, other): ... # Option 1 (most common)
@total_ordering
class B:
def __eq__(self, other): ...
def __le__(self, other): ... # Option 2
@total_ordering
class C:
def __eq__(self, other): ...
def __gt__(self, other): ... # Option 3
@total_ordering
class D:
def __eq__(self, other): ...
def __ge__(self, other): ... # Option 4
Missing eq Raises Error¶
from functools import total_ordering
@total_ordering
class Bad:
def __lt__(self, other): ...
# ValueError: must have at least one ordering operation defined
How Auto-Generation Works¶
Given __eq__ and __lt__, the decorator derives:
| Method | Derived As |
|---|---|
__le__(a, b) |
a < b or a == b |
__gt__(a, b) |
not (a < b or a == b) |
__ge__(a, b) |
not (a < b) |
# Conceptually equivalent to:
def __le__(self, other):
return self.__lt__(other) or self.__eq__(other)
def __gt__(self, other):
return not self.__le__(other)
def __ge__(self, other):
return not self.__lt__(other)
Practical Examples¶
Version Comparison¶
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, version_string):
self.parts = tuple(int(p) for p in version_string.split('.'))
def __eq__(self, other):
return self.parts == other.parts
def __lt__(self, other):
return self.parts < other.parts
def __repr__(self):
return f"Version({'.'.join(map(str, self.parts))})"
versions = [Version("2.0"), Version("1.10"), Version("1.2"), Version("3.0")]
print(sorted(versions))
# [Version(1.2), Version(1.10), Version(2.0), Version(3.0)]
print(Version("1.2") < Version("1.10")) # True
print(Version("2.0") >= Version("1.10")) # True
Money Type¶
from functools import total_ordering
@total_ordering
class Money:
def __init__(self, amount, currency="USD"):
self.amount = amount
self.currency = currency
def __eq__(self, other):
if self.currency != other.currency:
return NotImplemented
return self.amount == other.amount
def __lt__(self, other):
if self.currency != other.currency:
return NotImplemented
return self.amount < other.amount
def __repr__(self):
return f"Money({self.amount}, '{self.currency}')"
m1 = Money(100)
m2 = Money(200)
m3 = Money(100, "EUR")
print(m1 < m2) # True
print(m1 >= m2) # False
print(sorted([m2, m1])) # [Money(100, 'USD'), Money(200, 'USD')]
# Different currencies: returns NotImplemented → TypeError
# print(m1 < m3) # TypeError
Student Grades¶
from functools import total_ordering
@total_ordering
class Student:
def __init__(self, name, gpa):
self.name = name
self.gpa = gpa
def __eq__(self, other):
return self.gpa == other.gpa
def __lt__(self, other):
return self.gpa < other.gpa
def __repr__(self):
return f"{self.name}({self.gpa})"
students = [Student("Alice", 3.8), Student("Bob", 3.5), Student("Charlie", 3.9)]
print(sorted(students))
# [Bob(3.5), Alice(3.8), Charlie(3.9)]
print(max(students))
# Charlie(3.9)
Priority Queue Item¶
from functools import total_ordering
@total_ordering
class Task:
def __init__(self, priority, description):
self.priority = priority
self.description = description
def __eq__(self, other):
return self.priority == other.priority
def __lt__(self, other):
# Lower number = higher priority
return self.priority < other.priority
def __repr__(self):
return f"Task(p={self.priority}, '{self.description}')"
tasks = [Task(3, "Low"), Task(1, "Critical"), Task(2, "Normal")]
print(sorted(tasks))
# [Task(p=1, 'Critical'), Task(p=2, 'Normal'), Task(p=3, 'Low')]
Returning NotImplemented¶
When comparing incompatible types, return NotImplemented to let Python try the reflected operation:
from functools import total_ordering
@total_ordering
class Celsius:
def __init__(self, temp):
self.temp = temp
def __eq__(self, other):
if not isinstance(other, Celsius):
return NotImplemented
return self.temp == other.temp
def __lt__(self, other):
if not isinstance(other, Celsius):
return NotImplemented
return self.temp < other.temp
c = Celsius(100)
print(c == Celsius(100)) # True
print(c == "100") # False (Python handles NotImplemented)
print(c < Celsius(200)) # True
# print(c < "200") # TypeError (no reflected operation available)
Performance Considerations¶
Auto-generated methods involve an extra function call and boolean logic:
# Hand-written __le__ (1 comparison):
def __le__(self, other):
return self.celsius <= other.celsius
# Auto-generated __le__ (2 comparisons):
# Effectively: self.__lt__(other) or self.__eq__(other)
For most applications, this overhead is negligible. For performance-critical sorting of millions of objects, consider defining all methods manually:
# Performance-critical: define all methods
class FastTemperature:
def __init__(self, celsius):
self.celsius = celsius
def __eq__(self, other): return self.celsius == other.celsius
def __ne__(self, other): return self.celsius != other.celsius
def __lt__(self, other): return self.celsius < other.celsius
def __le__(self, other): return self.celsius <= other.celsius
def __gt__(self, other): return self.celsius > other.celsius
def __ge__(self, other): return self.celsius >= other.celsius
total_ordering vs Manual Implementation¶
| Aspect | @total_ordering |
Manual |
|---|---|---|
| Code | 2 methods | 5-6 methods |
| Consistency | Guaranteed | Possible bugs |
| Performance | Slight overhead | Optimal |
| Maintenance | Easy | Must update all methods |
| Best for | Most classes | Performance-critical sorting |
Relationship to lt and sorted()¶
Python's sorted() and list.sort() use __lt__ by default:
from functools import total_ordering
@total_ordering
class Item:
def __init__(self, value):
self.value = value
def __eq__(self, other):
return self.value == other.value
def __lt__(self, other):
return self.value < other.value
# sorted() works because __lt__ is defined
items = [Item(3), Item(1), Item(2)]
sorted(items) # Uses __lt__ internally
If you only need sorting (not general comparisons), defining just __lt__ is sufficient without @total_ordering.
Common Pitfalls¶
Inconsistent Equality and Ordering¶
from functools import total_ordering
@total_ordering
class Bad:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
return self.x == other.x # Compares only x
def __lt__(self, other):
return self.y < other.y # Orders by y
# Contradictory: a == b but a < b is possible!
a = Bad(1, 10)
b = Bad(1, 5)
print(a == b) # True (same x)
print(a < b) # False (a.y > b.y) — contradicts equality!
Rule: __eq__ and __lt__ should use the same attributes.
Forgetting hash¶
If you define __eq__, Python sets __hash__ to None, making instances unhashable:
@total_ordering
class Item:
def __init__(self, value):
self.value = value
def __eq__(self, other):
return self.value == other.value
def __lt__(self, other):
return self.value < other.value
# {Item(1)} # TypeError: unhashable type
# Fix: define __hash__
def __hash__(self):
return hash(self.value)
Summary¶
| Feature | Details |
|---|---|
| Import | from functools import total_ordering |
| Requires | __eq__ + one of __lt__, __le__, __gt__, __ge__ |
| Generates | The remaining 3 comparison methods |
| Performance | Slight overhead vs manual implementation |
| Use case | Any class that needs full comparison support |
Key Takeaways:
@total_orderingeliminates boilerplate: define 2 methods, get 6 comparisons- Always define
__eq__and__lt__(most common and expected pair) - Use the same attributes in
__eq__and__lt__to avoid contradictions - Return
NotImplementedfor incompatible types - Define
__hash__if instances need to go in sets or dict keys - For performance-critical sorting of millions of objects, define all methods manually
- If you only need
sorted()support,__lt__alone suffices without@total_ordering