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.
Mental Model
If you can test equality and one ordering direction, the other comparisons are logically derivable. @total_ordering does that derivation for you: define __eq__ plus any one of __lt__/__le__/__gt__/__ge__, and the decorator fills in the rest. It trades a small runtime cost for eliminating four boilerplate methods.
python
from functools import total_ordering
The Problem¶
Python's comparison operators each map to a separate dunder method:
```python 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¶
```python 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__
```python 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¶
```python 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) |
```python
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¶
```python 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¶
```python 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¶
```python 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¶
```python 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:
```python 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:
```python
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:
```python
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:
```python 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¶
```python 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:
```python @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
Exercises¶
Exercise 1.
Create a Temperature class with a celsius attribute. Use @total_ordering and define only __eq__ and __lt__. Verify that all six comparison operators (==, !=, <, <=, >, >=) work between two Temperature instances.
Solution to Exercise 1
from functools import total_ordering
@total_ordering
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def __eq__(self, other):
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius == other.celsius
def __lt__(self, other):
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius < other.celsius
def __repr__(self):
return f"Temperature({self.celsius})"
t1 = Temperature(20)
t2 = Temperature(30)
print(t1 == t2) # False
print(t1 != t2) # True
print(t1 < t2) # True
print(t1 <= t2) # True
print(t1 > t2) # False
print(t1 >= t2) # False
Exercise 2.
Build a Version class that represents semantic versions (e.g., Version(1, 2, 3) for 1.2.3). Use @total_ordering with __eq__ and __lt__ comparing (major, minor, patch) tuples. Sort a list of versions and verify the order.
Solution to Exercise 2
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, major, minor, patch):
self.major = major
self.minor = minor
self.patch = patch
def _key(self):
return (self.major, self.minor, self.patch)
def __eq__(self, other):
if not isinstance(other, Version):
return NotImplemented
return self._key() == other._key()
def __lt__(self, other):
if not isinstance(other, Version):
return NotImplemented
return self._key() < other._key()
def __repr__(self):
return f"{self.major}.{self.minor}.{self.patch}"
versions = [Version(2, 0, 0), Version(1, 9, 1), Version(1, 10, 0), Version(1, 9, 0)]
print(sorted(versions))
# [1.9.0, 1.9.1, 1.10.0, 2.0.0]
Exercise 3.
Create a Student class with name and gpa attributes. Use @total_ordering, comparing by gpa. Define __hash__ based on name so students can be stored in a set. Demonstrate sorting a list of students and adding them to a set.
Solution to Exercise 3
from functools import total_ordering
@total_ordering
class Student:
def __init__(self, name, gpa):
self.name = name
self.gpa = gpa
def __eq__(self, other):
if not isinstance(other, Student):
return NotImplemented
return self.gpa == other.gpa
def __lt__(self, other):
if not isinstance(other, Student):
return NotImplemented
return self.gpa < other.gpa
def __hash__(self):
return hash(self.name)
def __repr__(self):
return f"Student({self.name!r}, {self.gpa})"
students = [Student("Alice", 3.8), Student("Bob", 3.5), Student("Charlie", 3.9)]
print(sorted(students))
# [Student('Bob', 3.5), Student('Alice', 3.8), Student('Charlie', 3.9)]
student_set = set(students)
print(student_set) # All three (unique names)