When do you need functools.cmp_to_key instead of a plain key function for sorting?
answer
- Python 3 dropped an old sort parameter
- Two arguments in, a sign out
- It wraps each element in a proxy
- Cost is per comparison, not per element
- Only for rules that compare pairs
basics
~20 sOnly when the ordering rule is genuinely pairwise and cannot be expressed as a value computed from one element alone, or when porting a legacy two-argument comparator. functools.cmp_to_key wraps that comparator so sorted() can use it, at a real performance cost.
solid answer
~50 sPython 3 removed the `cmp` parameter from sorting, leaving only `key`. `functools.cmp_to_key` bridges the gap: it takes a two-argument comparator returning a negative number, zero or a positive number, and returns a callable suitable for `key=`. Under the hood each element is wrapped in a small proxy object whose comparison methods delegate to your comparator, so the comparator runs on **every comparison** — roughly O(n log n) Python-level calls plus one wrapper allocation per element — where a normal key runs `n` times. Reach for it only when the rule is inherently **relational**, meaning the answer for two elements cannot be derived from either one alone, or when you must preserve a legacy comparator's exact behaviour. Almost every real ordering is better expressed as a tuple key, a negated numeric field, or a normalized string.
code
python · 11 linesfrom functools import cmp_to_key
def compare(a, b):
if len(a) != len(b):
return len(b) - len(a) # longer first
return (a > b) - (a < b) # then alphabetical
print(sorted(["bb", "a", "ccc", "ab"], key=cmp_to_key(compare)))
# the same order without a comparator, and n key calls instead of n log n
print(sorted(["bb", "a", "ccc", "ab"], key=lambda s: (-len(s), s)))go deeper
Just know that Python 3 sorts with a key function, not a two-argument comparator, and that functools.cmp_to_key exists as a bridge for old comparator code you may meet.
Explain the comparator contract — negative, zero, positive — the idiomatic (a > b) - (a < b) expression, and why you would still prefer a tuple key for anything decomposable per element.
Show the cost model out loud: n log n Python-level calls plus a wrapper per element versus n key calls, and name the narrow relational orderings that actually justify paying it.
Own the migration and consistency angle: inherited comparators encode business rules that may not be total orders, and an inconsistent one fails silently — decide when to wrap them and when to rewrite the rule as a key with tests.
## Why the function exists Early Python let you pass a comparator to `sort`, in the style of C's `qsort`: a function of two arguments returning negative, zero or positive. Python 3 removed that parameter and kept only `key`, because key functions are both faster and easier to reason about. `functools.cmp_to_key` was added as the migration bridge and has stayed, because a small number of orderings genuinely need pairwise logic. ## What it does mechanically `cmp_to_key(comparator)` returns a callable you pass as `key=`. When the sort computes the key of an element, it gets back a **proxy object** holding that element together with the comparator. The proxy implements the comparison operations by calling your comparator on the two wrapped values and interpreting the sign of the result. So the sort still compares keys — it is just that comparing these keys means running your Python function. The cost follows directly. A normal key does `n` calls and then compares cheap precomputed values in C. A `cmp_to_key` sort allocates `n` proxy objects and makes roughly `n log n` Python-level comparator calls, each with its own frame. On a small list this is irrelevant; on a large one it is a multiple, not a percentage. ## The comparator contract Your comparator must return a negative number when the first argument sorts first, zero when the two are equivalent, and a positive number otherwise. The idiomatic three-way expression for an existing total order is `(a > b) - (a < b)`, which yields exactly -1, 0 or 1. Subtracting numbers (`a - b`) also works for plain numeric fields but is a trap for floats near zero and for anything that is not a number. The comparator must also be **consistent**: if it is not a genuine total order — if it says A before B, B before C and C before A — the sort will not raise, it will simply produce arbitrary output. That silent failure mode is one more reason to prefer keys, which cannot express an inconsistent order in the first place. ## When a key genuinely cannot do the job Ask one question: *can I compute, from a single element alone, a value whose natural ordering reproduces my rule?* If yes, write that key. The cases where the answer is no are relational — the correct order of two elements depends on comparing them against each other rather than on anything intrinsic. Ordering a set of text fragments so that their concatenation is extremal is the textbook example: whether one fragment belongs before another depends on both fragments joined in each order. Domain rules like "a record supersedes another when its revision chain contains it" are the same shape. The second legitimate case is inheritance: an existing comparator, possibly ported from another language or from Python 2, whose exact behaviour — including its quirks — must be preserved while the calling code moves forward. Wrapping it is safer than rewriting the ordering rule from memory. ## Alternatives to reach for first - **Tuple keys** for multi-field ordering, with a negated numeric component for a descending field. - **Two stable passes**, least significant field first, when a non-numeric field must run descending. - **Normalized text keys** for case- and accent-insensitive ordering: `str.casefold`, optionally combined with `unicodedata.normalize`. For locale-aware collation, `locale.strxfrm` is designed to be used exactly as a key function. - **A precomputed sort field** stored on the record, when the key computation is expensive or shared across many call sites. ## In an interview This is a differentiator rather than a gate. The strong answer is not that you have memorized the signature; it is that you can state *why* Python moved to key functions — `n` calls instead of `n log n`, no way to express an inconsistent order — and then name the narrow class of relational orderings that still justifies a comparator. A candidate who reaches for `cmp_to_key` to sort by two fields has told you they have not internalized tuple keys.
- What does functools.cmp_to_key actually return, and why is it slower than a plain key?It returns a callable that wraps each element in a proxy object whose comparison methods delegate to your comparator. The sort therefore calls your Python function on every comparison — about n log n times — and allocates one wrapper per element, where an ordinary key function runs n times and leaves the comparisons to C. On large inputs that is a multiple of the runtime, not a small percentage.
- Give an ordering that honestly cannot be expressed as a key function.One where the correct order of two elements depends on examining both together rather than on anything computable from one alone — arranging text fragments so their concatenation is extremal is the classic shape, since whether one fragment precedes another depends on the two joined in each order. Domain rules such as "this revision supersedes that one" behave the same way. Anything decomposable into per-element values belongs in a tuple key.
- What happens if the comparator you wrap is not a consistent total order?Nothing visible: no exception is raised, and the sort simply produces some arbitrary arrangement. If the comparator claims A before B, B before C and C before A, the result depends on the comparison sequence the algorithm happens to make. That silent failure is a strong argument for key functions, which cannot express an inconsistent order at all.
saying these in an interview costs you the question
- Uses cmp_to_key for ordinary multi-field sorting
- Thinks the comparator must return True or False
- Believes cmp_to_key performs the same as a key function
- Says Python 3 still accepts a cmp argument to sorted
- Assumes an inconsistent comparator raises an error