A chat archiver's functools.cmp_to_key comparator returns 0 whenever it hits an error — what breaks?
answer
- Zero is a claim, not an abstention
- The order stops being a consistent total order
- Nothing raises, nothing is logged
- Stability makes the result depend on input order
- A key function fails on its own element
basics
~20 sSwallowing the error and returning 0 tells the sort those two messages are tied. The order silently stops being a consistent total order, so transcripts come out shuffled in a way that depends on input order, and nothing raises.
solid answer
~50 sReturning `0` from an `except` block is not "skip this pair" — it is a positive claim that the two messages are equal in order. Once some pairs are wrongly tied, the comparator no longer defines a consistent total order, and the sort's merges combine runs on contradictory information. CPython performs no consistency check, so there is no exception and no warning: you get a plausible-looking list in the wrong order. Because the sort is stable, wrongly-tied elements keep their **input** order, which makes the corruption depend on the arrival order of the archive — it reproduces on one file and vanishes on another, which is why it survived a three-week release train unnoticed. The fix is to let the bad record raise, normalize timestamps at ingest, and express the ordering as a key function so a malformed value fails loudly on its own element.
code
python · 13 linesfrom functools import cmp_to_key
def cmp_message(a, b):
try:
return (a["ts"] > b["ts"]) - (a["ts"] < b["ts"])
except TypeError:
return 0
rows = [{"ts": 3}, {"ts": None}, {"ts": 1}, {"ts": 2}]
print([r["ts"] for r in sorted(rows, key=cmp_to_key(cmp_message))])
print([r["ts"] for r in sorted(reversed(rows), key=cmp_to_key(cmp_message))])go deeper
Recall that a comparator's zero means the two items are tied, not that the comparison was skipped, and that catching an exception to return zero invents an ordering claim the data does not support.
Explain why a wrongly-tied pair corrupts more than that pair: the sort's merges act on contradictory answers, and stability then makes the surviving order depend on how the input happened to be arranged.
Show the diagnosis path — instrument the swallowed branch, re-sort with a strict comparator, re-run with shuffled input — and argue for moving the ordering to a key function and the parsing to ingest so failures are loud and attributable.
Own the policy: error handlers must never fabricate values on a correctness path, malformed records need a defined destination rather than a silent tie, and data contracts belong at the boundary rather than in whatever code first trips over them.
## The scenario A chat-transcript archiver merges message batches from several sources and sorts them before writing a day's transcript. The ordering was ported from an older tool as a two-argument comparator and adapted with `functools.cmp_to_key`. Somewhere along the way a defensive `except` was added: ```python def cmp_message(a, b): try: return (a["ts"] > b["ts"]) - (a["ts"] < b["ts"]) except TypeError: return 0 # "don't let one bad record break the job" ``` Three weeks after the release train that shipped it, support reports that some archived transcripts read out of order — replies before the messages they answer. Nothing failed. No exception was logged. The job's exit status was zero every night. ## What actually broke `0` does not mean "I could not compare these". To a sort, `0` means **these two are equal in order** — a positive, load-bearing claim. The moment one record has a `None` or a string timestamp where the others have numbers, every comparison involving it returns `0`, and the comparator starts asserting that a broken message ties with *everything*. That destroys the property a sort requires: a **consistent total order**, where the relation is transitive and any two elements are related by exactly one of "before", "tied" or "after". Here `bad == x` and `bad == y` while `x < y`, which is a contradiction. Python's sort is a merge sort that builds runs and merges them; each merge trusts the comparisons it makes and does not re-check earlier ones. Fed contradictory answers it still terminates and still returns a list of the right length with all the original elements — just not in a correct order, and the misplacement is not confined to the malformed record. Good messages end up on the wrong side of the tie and travel with it. Two properties make this a nightmare to diagnose. First, **CPython does not detect it**: there is no validation of the comparator and no error when merges disagree, so the failure has no signal at all. Second, the sort is **stable**, so wrongly-tied elements retain their relative *input* order. The output therefore depends on the order in which batches happened to be concatenated — which changes with source availability and retry timing. The bug reproduces on Tuesday's archive and not on Wednesday's, looks like a race, and survives review because the `except` clause reads like defensive programming. ## Confirming it Start by proving the fallback branch is being taken at all, since by construction it leaves no trace. Replace the bare `return 0` with a counter and a log line — `logging.exception` in that `except` gives you the type and the offending record. If the count is non-zero on real input, you have your answer without any further theory. Then confirm the ordering claim independently. Re-sort a captured batch with a strict comparator that re-raises instead of swallowing; if it now raises `TypeError` on a record whose timestamp is `None` or a `str`, the malformed data is confirmed. A second check is to re-run the same batch with its input order shuffled: a correct total order gives the same output every time apart from genuine ties, so output that changes with input order is proof that elements are being wrongly tied. ## Fixing it The immediate fix is to stop lying. A comparator that cannot compare two elements should let the exception propagate: the sort aborts, you see the record, and — importantly — a partially sorted `list.sort` leaves the list in a modified but valid state, so operate on a copy or use `sorted()` if you intend to recover. The better fix is structural, and it also removes the adapter's cost. This ordering is a plain single-field ordering, so it belongs in a key function, not a comparator: ```python import operator rows.sort(key=operator.itemgetter("ts")) ``` A key function is evaluated once per element, so a malformed timestamp raises immediately, attached to *its own* record rather than to a pair, and the traceback names the value. It also removes one Python-level call per comparison, which on a day's transcript volume is the difference between a noticeable pause and none. Where the ordering is genuinely multi-field — timestamp then source then sequence — a tuple key expresses it directly. The durable fix is at the boundary: parse and normalize timestamps at ingest, so that everything downstream holds one type. Comparison-time coercion is the wrong layer; it turns a data-quality problem into an ordering problem and puts the guess in the hottest code path in the job. ## The rule to carry away `except: return 0` inside a comparator is not resilience, it is fabricated data. Any error handler that must produce an answer where none exists should either raise, or route the element to an explicit, documented position — for example sorting unparseable records last by a key that says so — so the choice is visible in the output instead of hidden in an exception handler.
- Does CPython raise anything when a comparator contradicts itself?No. There is no consistency check in `list.sort` or `sorted`; the merge trusts every comparison it makes. The sort terminates and returns all the original elements, simply in a wrong order. Some other language runtimes detect contradictory merges and throw, which is why engineers coming from them are surprised that Python's failure mode is silent.
- If a comparison raises in the middle of list.sort(), what state is the list left in?Partially modified, with no ordering guarantee — the documentation says as much. During the sort the list also appears empty to any other code that touches it, and mutating it mid-sort raises `ValueError`. If you need the original on failure, sort a copy or use `sorted()`, which builds a new list and leaves the input untouched.
- Where should a malformed timestamp be caught if not in the comparator?At ingest. Parse and normalize into one type as records enter the archiver, and reject or quarantine what does not parse. That keeps the ordering path free of type guesses, makes the failure attributable to a specific record and source, and means the sort works on data that is already known to be comparable.
A referee who signals "dead heat" every time the photo-finish camera fails does not skip the race — they record a result, and the whole standings table is quietly wrong.
saying these in an interview costs you the question
- Treating a 0 return as "skip this pair"
- Assuming the sort will raise on a contradictory comparator
- Believing only the malformed record is misplaced
- Calling the wandering output a threading race
- Adding a try/except in the comparator as resilience
- Sorting in place and expecting the list intact after a raise