total_ordering: Generate Consistent Comparisons

Published on: August 29, 2026
Reading time: 5 minutes
A person reads 'Python for Unix and Linux System Administration' indoors.

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, then b == a.
  • Transitivity: if a < b and b < c, then a < c.
  • Coherence: a <= b matches a < 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.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Close-up view of a computer screen displaying code in a software development environment.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    inspect.signature: Inspect Function Parameters

    Learn Python inspect.signature to read parameters, bind arguments, preserve decorators, and build dynamic callable interfaces safely.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    Vivid close-up of a python resting among autumn leaves, showcasing its intricate patterns.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    get_origin and get_args: Inspect Generic Types

    Learn Python get_origin and get_args to inspect generics, unions, Annotated, Literal, aliases, and runtime type metadata safely.

    Ler mais

    Tempo de leitura: 6 minutos
    29/08/2026
    Detailed shot of a Jungle Carpet Python (Morelia spilota cheynei) in its natural habitat.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python LiteralString: Trusted Strings

    Learn Python LiteralString to restrict SQL, templates, and commands to trusted strings and reduce injection risks with static analysis.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    A detailed view of computer programming code on a screen, showcasing software development.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python dataclass_transform: Generated Classes

    Learn Python dataclass_transform to type decorators, base classes, and metaclasses that generate fields, __init__, and methods.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    A close-up shot showcasing the intricate scales of a snake, highlighting texture and color.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python TypeVarTuple: Variadic Generics

    Learn Python TypeVarTuple to preserve heterogeneous tuples, model dimensions, and build generics with variable type parameters.

    Ler mais

    Tempo de leitura: 4 minutos
    29/08/2026
    A developer typing code on a laptop with a Python book beside in an office.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    assert_type and reveal_type: Test Type Inference

    Learn Python assert_type and reveal_type to inspect inference, test typed APIs, and prevent static typing regressions.

    Ler mais

    Tempo de leitura: 4 minutos
    29/08/2026