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.
Recommended tests
cmp(a, a) == 0.- The signs of
cmp(a, b)andcmp(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.







