cmp_to_key: Use Legacy Comparators with sorted

Published on: August 29, 2026
Reading time: 5 minutes
A developer typing code on a laptop with a Python book beside in an office.

Modern Python sorting prefers key functions because a key is computed once per element and then compared efficiently. Legacy systems, external libraries, and language-aware collation APIs sometimes provide a two-argument comparator that returns a negative number, zero, or a positive number. functools.cmp_to_key() adapts that comparator to the key interface accepted by sorted(), list.sort(), and related operations.

This guide explains how the adapter works, how to migrate old comparators, preserve stability, sort with locale rules, add deterministic tie breakers, avoid inconsistent relations, and decide when to replace a comparator with an ordinary key function.

A three-way comparator

def compare(a, b):
    if a < b:
        return -1
    if a > b:
        return 1
    return 0

Only the sign matters. A comparator does not need to return exactly -1 or 1. Any negative result means “a comes before b,” zero means equivalent for sorting, and any positive result means “a comes after b.”

Using cmp_to_key

from functools import cmp_to_key

values = [10, 2, 30, 4]
ordered = sorted(values, key=cmp_to_key(compare))

cmp_to_key() creates a wrapper class. Each wrapper stores one original value and implements rich comparisons by calling the supplied comparator.

Why key functions are preferred

sorted(people, key=lambda person: person.name.casefold())

A key function is evaluated once per item. A comparator may be called many times during sorting. When normalization is expensive, the performance difference can be substantial.

Migrating legacy code

def compare_products(a, b):
    if a.price != b.price:
        return -1 if a.price < b.price else 1
    return -1 if a.name < b.name else (1 if a.name > b.name else 0)

products.sort(key=cmp_to_key(compare_products))

The adapter keeps existing behavior while a system is modernized. The same relation is usually clearer as:

products.sort(key=lambda product: (product.price, product.name))

The tuple key is shorter, faster, and easier to test.

Descending order

For a complete reverse order, use reverse=True. For mixed directions, transform only the relevant key component. Avoid manually flipping comparator signs unless the comparator is imposed by another API.

sorted(products, key=lambda product: product.price, reverse=True)

Deterministic tie breakers

A comparator should return zero only when elements are equivalent for that ordering. Add tie breakers when deterministic output matters:

def compare_tasks(a, b):
    if a.priority != b.priority:
        return b.priority - a.priority
    if a.created_at != b.created_at:
        return -1 if a.created_at < b.created_at else 1
    return (a.id > b.id) - (a.id < b.id)

The final expression turns Boolean comparisons into -1, 0, or 1 without subtracting potentially large values.

Stable sorting

Python sorting is stable: elements considered equivalent retain their original relative order. When a comparator returns zero, this property can be used for multi-pass sorting.

records.sort(key=lambda record: record.name)
records.sort(key=lambda record: record.department)

After the second pass, names remain ordered inside each department.

Locale-aware sorting

A classic use case adapts locale.strcoll:

import locale
from functools import cmp_to_key

locale.setlocale(locale.LC_COLLATE, "")
names = sorted(names, key=cmp_to_key(locale.strcoll))

strcoll compares two strings using the active locale. For performance, locale.strxfrm is often preferable as a key function:

names = sorted(names, key=locale.strxfrm)

Locale is process-global state and can be problematic in concurrent servers. Establish a clear policy or use a collation library with isolated configuration.

Non-transitive comparators

A valid ordering must be transitive. If A comes before B and B before C, A should come before C. Cyclic rules create an unsuitable ordering:

# rock < paper, paper < scissors, scissors < rock

Rock-paper-scissors is a game relation, not a total order for sorting.

Antisymmetry of the sign

The sign of cmp(a, b) should be the opposite of cmp(b, a). If both calls return a negative value, the sorting algorithm receives contradictory information.

Consistency with equality

Items may be equivalent for sorting without being equal objects, but that choice must be intentional. A case-insensitive comparator may treat “Ana” and “ana” as equivalent. Stable sorting then preserves their input order.

Do not return Boolean values

def wrong(a, b):
    return a < b

Booleans are integers 0 and 1. This function never returns a negative result and violates the contract. A compact correct pattern is:

def compare(a, b):
    return (a > b) - (a < b)

None and missing values

Define where missing values belong:

def compare_none(a, b):
    if a is None and b is None:
        return 0
    if a is None:
        return 1
    if b is None:
        return -1
    return (a > b) - (a < b)

An equivalent key is often key=lambda value: (value is None, value).

Heterogeneous values

Python 3 does not impose a default order between numbers, strings, and arbitrary objects. A comparator can define category rules, but they must be documented and deterministic.

def category(value):
    if isinstance(value, (int, float)):
        return 0
    if isinstance(value, str):
        return 1
    return 2

A compound key such as (category(value), normalized_value) is usually safer.

Version strings

Lexicographic order places “10” before “2”. A comparator can split components, but a transformation key is better:

def version_key(text: str):
    return tuple(int(part) for part in text.split("."))

Comparators must be pure

Do not mutate objects, increment external counters used by logic, or change global configuration inside the comparator. The algorithm may compare the same pair multiple times and in different directions. Side effects make the result depend on implementation details.

Exceptions during sorting

If a comparator raises an exception, sorting stops. With in-place list.sort(), the list may already be partially rearranged. Validate inputs first and keep the comparator small and predictable.

Performance model

Sorting n values requires approximately O(n log n) comparisons. cmp_to_key adds Python wrapper objects and function calls. A key function computes O(n) transformed values and comparisons often happen in optimized built-in types.

Caching expensive transformations

If you cannot change the comparator API, cache expensive normalization by immutable value or object identity. Be careful with memory and mutation. In most cases, decorate-sort-undecorate is simpler: calculate keys once, sort key-value pairs, and extract the original objects.

Using reverse with cmp_to_key

sorted(items, key=cmp_to_key(compare), reverse=True)

reverse=True reverses the final order while preserving stability. Confirm whether you intend to reverse the entire relation or only one criterion.

  • cmp(a, a) == 0.
  • The signs of cmp(a, b) and cmp(b, a) are opposite.
  • Transitivity across triples.
  • Equivalent items retain their original order.
  • Edge cases for None, NaN, empty strings, and extreme numbers.
  • The result matches a reference key function when one exists.

Common mistakes

  • Returning bool: the contract requires negative, zero, or positive.
  • Adding side effects: results become path-dependent.
  • Ignoring transitivity: the relation is not sortable.
  • Using cmp_to_key for ordinary new code: key functions are normally better.
  • Changing locale in a concurrent server: locale is global state.
  • Repeating expensive normalization: precompute a key.

Complete example: natural filename order

import re
from functools import cmp_to_key

_pattern = re.compile(r"(\d+)")

def parts(text: str):
    return [
        int(part) if part.isdigit() else part.casefold()
        for part in _pattern.split(text)
    ]

def natural_compare(a: str, b: str) -> int:
    left = parts(a)
    right = parts(b)
    return (left > right) - (left < right)

files = ["item10.txt", "item2.txt", "item1.txt"]
print(sorted(files, key=cmp_to_key(natural_compare)))

Because parts() is recalculated many times, the recommended implementation is simply sorted(files, key=parts). The comparator example demonstrates adaptation, not the ideal design for new code.

When cmp_to_key is appropriate

Use it when integrating a required comparator from legacy code, an external protocol, or an API such as strcoll. For new code under your control, prefer key functions, tuples, and reverse=True.

Conclusion

functools.cmp_to_key() bridges two-argument comparators and Python’s modern key-based sorting model. It preserves existing rules, but it cannot repair an inconsistent comparator and usually costs more than computing one key per value.

The official Python cmp_to_key documentation defines the adapter. Use it for compatibility, test ordering properties, and migrate to key functions whenever possible.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    A person reads 'Python for Unix and Linux System Administration' indoors.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    total_ordering: Generate Consistent Comparisons

    Learn Python total_ordering to generate consistent comparisons, return NotImplemented, integrate dataclasses, and test ordering rules.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    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