Domain classes often need a natural order: versions, priorities, ranges, products, timestamps, or ranked records. Manually implementing __lt__, __le__, __gt__, and __ge__ creates repetition and increases the chance of contradictory behavior. The functools.total_ordering decorator fills in missing ordering methods when a class provides __eq__ and at least one rich ordering method.
This guide explains how to design a correct total order, return NotImplemented, compare compound keys, integrate with dataclasses, handle inheritance and special values, measure performance, and test mathematical properties.
The repetition problem
class Version:
def __init__(self, major: int, minor: int):
self.major = major
self.minor = minor
Supporting every ordering operator would require several nearly identical methods. A small difference between them can make a < b disagree with a >= b.
Your first total_ordering class
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, major: int, minor: int):
self.major = major
self.minor = minor
def __eq__(self, other: object) -> bool:
if not isinstance(other, Version):
return NotImplemented
return (self.major, self.minor) == (other.major, other.minor)
def __lt__(self, other: object) -> bool:
if not isinstance(other, Version):
return NotImplemented
return (self.major, self.minor) < (other.major, other.minor)
With equality and less-than defined, the decorator adds the missing operators. Tuple comparison centralizes the ordering key and uses Python’s lexicographic semantics.
What the decorator generates
total_ordering looks for one of __lt__, __le__, __gt__, or __ge__. Combined with __eq__, that method is enough to synthesize the remaining operations. Existing or inherited methods are not overwritten.
Why NotImplemented matters
def __lt__(self, other: object) -> bool:
if not isinstance(other, Version):
return NotImplemented
return self.key < other.key
NotImplemented is not the same as False. It tells Python that this operand combination is unsupported and allows the reflected operation on the other object to run. If neither object knows how to compare, Python raises TypeError.
Do not raise NotImplemented
NotImplemented is a special return value. NotImplementedError is an exception for methods that have not been implemented. Rich comparison methods normally return the value rather than raising the exception.
Centralizing the comparison key
@property
def key(self) -> tuple[int, int]:
return self.major, self.minor
A property or private method prevents duplication between __eq__ and __lt__. The key should contain exactly the fields that define ordering identity. If equality and ordering use different fields, the relation may become inconsistent.
Total order versus partial order
A total order expects comparable elements to be consistently less than, equal to, or greater than one another. Some domains have only a partial order. Sets, dependency graphs, capabilities, and permissions may have incomparable values. Forcing a total order on those objects can create a misleading rule.
Comparing different types
version = Version(3, 12)
# version < 10 should raise TypeError, not silently return False
Returning False for every incompatible object falsely suggests that the values participate in one ordering. Return NotImplemented and let Python follow the comparison protocol.
Subclasses
isinstance(other, Version) accepts subclasses. That is useful when all subclasses share the same comparison semantics. If a subclass adds fields that change identity, cross-class comparison may violate symmetry. Strict domains can require type(other) is type(self).
Equality and hashing
Defining __eq__ may make a class unhashable because custom equality requires a compatible hash. Immutable objects used as dictionary keys can implement __hash__ from the same key:
def __hash__(self) -> int:
return hash(self.key)
Never derive a hash from mutable fields that can change after insertion into a set or dictionary.
Dataclass integration
from dataclasses import dataclass
@dataclass(order=True, frozen=True)
class Version:
major: int
minor: int
When ordering follows field declaration order, @dataclass(order=True) is usually simpler and generates equality and ordering together. The Python dataclasses guide covers compare=False, frozen instances, and field customization.
When total_ordering is better than dataclass
Use total_ordering when the class is not a dataclass, the comparison key requires normalization, fields use a different order, or operand compatibility needs explicit checks.
Priority example
@total_ordering
class Task:
def __init__(self, priority: int, created_at: float, title: str):
self.priority = priority
self.created_at = created_at
self.title = title
@property
def key(self):
return (-self.priority, self.created_at, self.title)
def __eq__(self, other):
if not isinstance(other, Task):
return NotImplemented
return self.key == other.key
def __lt__(self, other):
if not isinstance(other, Task):
return NotImplemented
return self.key < other.key
Negating priority places larger priority values first when the collection is sorted in ascending order. Creation time and title create deterministic tie breakers.
Special floating-point values
NaN does not behave like an ordinary value in a total order. Comparisons can be false even against itself. If the domain accepts NaN, reject it, normalize it, or define an explicit policy before building the comparison key.
None and sentinel values
Python 3 does not automatically order None and numbers. Convert missing values into a comparable compound key:
key = (value is None, value if value is not None else 0)
The first component controls whether missing values appear first or last. Document the policy.
Strings and locale
String comparison follows Unicode code points rather than complete human-language collation. For user-facing order, normalize case and accents or use an appropriate locale-aware key. Equality and ordering must agree about the normalization.
Performance
Generated methods may add extra calls and produce deeper stack traces. In most business applications the difference is insignificant. In a hot loop with millions of comparisons, explicit methods may be faster and easier to profile.
Inheritance and existing methods
The decorator does not replace a method already defined on the class or inherited from a base. A base class can therefore contribute an ordering method that disagrees with the subclass implementation. Inspect the hierarchy and keep comparison semantics in one clear layer.
Sorting collections
versions = [Version(3, 12), Version(3, 10), Version(4, 0)]
print(sorted(versions))
sorted() primarily relies on __lt__. total_ordering is most valuable when consumers also use <=, >, and >=. If ordering is needed only in one location, a key= function may be simpler.
Prefer key functions for multiple views
sorted(products, key=lambda product: (product.price, product.name))
Do not impose a global order when different screens sort by price, name, rating, or creation date. Rich comparison should express one stable natural order of the domain.
Testing ordering properties
Test more than a few examples:
- Equality reflexivity:
a == a. - Symmetry: if
a == b, thenb == a. - Transitivity: if
a < bandb < c, thena < c. - Coherence:
a <= bmatchesa < b or a == b. - Unsupported operands return NotImplemented and lead to TypeError.
Property-based tests are effective for discovering edge cases, tie-breaking errors, and special values.
Common mistakes
- Returning False for an incompatible type: return NotImplemented.
- Using different fields in eq and lt: ordering may contradict equality.
- Forcing a total order onto a partial domain: expose explicit relations instead.
- Ignoring NaN: special floats violate ordinary expectations.
- Defining one global order for every screen: use key functions.
- Forgetting hash semantics: custom equality affects dict and set usage.
Complete example: semantic version value
from functools import total_ordering
@total_ordering
class Version:
__slots__ = ("major", "minor", "patch")
def __init__(self, major: int, minor: int, patch: int = 0):
self.major = major
self.minor = minor
self.patch = patch
@property
def key(self) -> tuple[int, int, int]:
return self.major, self.minor, self.patch
def __eq__(self, other: object) -> bool:
if type(other) is not type(self):
return NotImplemented
return self.key == other.key
def __lt__(self, other: object) -> bool:
if type(other) is not type(self):
return NotImplemented
return self.key < other.key
def __hash__(self) -> int:
return hash(self.key)
def __repr__(self) -> str:
return f"Version{self.key}"
The class uses one key for equality, ordering, and hashing. Different runtime types are not compared silently.
Conclusion
functools.total_ordering reduces boilerplate and concentrates comparison semantics in equality plus one fundamental ordering operator. It works best when a domain has a real total order and incompatible operands return NotImplemented.
The official Python total_ordering documentation describes the generated methods. Use consistent keys, test mathematical properties, and implement every operator manually only when performance or debugging clarity justifies the extra code.







