skip to content

How do you make sorted() order one field descending and another ascending?

level: seniorimportance: should knowfreq 45%

answer

  1. Two fields, opposite directions
  2. One boolean cannot cover both
  3. Negation works only on numbers
  4. Equal elements keep their relative order
  5. Sort the least significant field first

basics

~10 s

reverse= flips the whole ordering, never one field. Either negate the numeric field inside a tuple key, as in key=lambda r: (-r.score, r.name), or run two stable sorts, least significant field first.

solid answer

~40 s

`reverse` is a single boolean over the entire sort, so per-field directions need one of two techniques. When the descending field is **numeric**, negate it inside a tuple key: `key=lambda r: (-r[1], r[0])` gives score descending, name ascending, in one pass. When it is not numeric — a `str`, a `date` — negation has no meaning, so use the general technique: Python's sort is **guaranteed stable**, so sort by the least significant field first, then re-sort by the most significant with `reverse=True`; the second pass leaves the first pass's order intact among ties. The senior detail is that `reverse=True` is *not* `reversed(sorted(...))`: `reverse=True` preserves the original order of tied elements, while reversing the output flips them and destroys the earlier pass.

code

python · 6 lines
python
rows = [("Springfield", 0.92), ("Ashland", 0.92), ("Bristol", 0.75)]

print(sorted(rows, key=lambda r: (-r[1], r[0])))

by_name = sorted(rows, key=lambda r: r[0])
print(sorted(by_name, key=lambda r: r[1], reverse=True))

go deeper

for a junior

Know that reverse=True flips the entire ordering rather than one field, and that a tuple key (a, b) orders by a first and breaks ties with b.

for a middle

Explain both idioms: negate the numeric component inside a tuple key, or run two stable sorts starting with the least significant field. Be able to say why the negation trick fails on strings and dates.

for a senior

Demonstrate that reverse=True preserves the order of tied elements while reversing the output does not, and connect that to top-k slices and cutoffs that must be reproducible across runs.

for a principal

Decide where a multi-key ordering is defined at all: one documented, shared sort key versus lambdas drifting at every call site. Insist that ties be broken explicitly by a unique field rather than left to stability.

### Why `reverse=` alone cannot do it `sorted()`'s `reverse` argument is a single boolean that flips the **entire** ordering. There is no per-field version of it, and no way to pass a direction per component of a tuple key. So "confidence descending, place name ascending" needs one of two techniques. ### Technique 1 — negate the numeric component of a tuple key A tuple key compares element by element, so `(-score, name)` sorts by `-score` ascending — which is score descending — and breaks ties by name ascending: ```python rows = [("Springfield", 0.92), ("Ashland", 0.92), ("Bristol", 0.75)] sorted(rows, key=lambda r: (-r[1], r[0])) # [('Ashland', 0.92), ('Springfield', 0.92), ('Bristol', 0.75)] ``` One pass, one key call per element, easy to read. Its limit is exactly the word *numeric*: you can negate an `int`, a `float`, a `Decimal` or a `Fraction`, but there is no negation of a `str`, a `datetime`, a `date`, a `tuple`, or of `None`. A date can be pushed through the trick by converting it to a numeric timestamp first; a string cannot be, and candidates who try to invent one (reversing the characters, subtracting ordinals) produce something that is wrong for anything but fixed-length ASCII. ### Technique 2 — two stable passes, least significant field first Python's sort is **guaranteed stable**: elements that compare equal keep their original relative order. That guarantee is documented and has held since Python 2.2, and it is unchanged through 3.14. Stability turns a multi-field ordering into a sequence of single-field sorts, provided you run them from the **least** significant field to the **most** significant: ```python rows = sorted(rows, key=lambda r: r[0]) # secondary: name ascending rows = sorted(rows, key=lambda r: r[1], reverse=True) # primary: score descending ``` The second pass reorders by score; among records with the same score it changes nothing, so the name ordering established by the first pass survives. This composes to any number of fields with any mix of directions, and it is the general answer — the tuple trick is the special case that happens to be shorter when every descending field is a number. The cost is real: k passes are k sorts, so 2x the comparison work here, plus a fresh list per pass. For a few hundred thousand rows that is usually irrelevant next to readability; for a hot path, prefer the single tuple key when the types allow it. ### The subtlety that separates a senior answer: `reverse=True` is not `reversed()` `sorted(data, key=f, reverse=True)` is **not** the same as reversing an ascending sort. `reverse=True` is defined to preserve the original relative order of equal elements — stability holds in the descending direction too — whereas `reversed(sorted(...))` flips ties along with everything else. ```python rows = [("Springfield", 0.92), ("Ashland", 0.92), ("Bristol", 0.75)] sorted(rows, key=lambda r: r[1], reverse=True) # [('Springfield', 0.92), ('Ashland', 0.92), ('Bristol', 0.75)] list(reversed(sorted(rows, key=lambda r: r[1]))) # [('Ashland', 0.92), ('Springfield', 0.92), ('Bristol', 0.75)] ``` This is where the multi-pass technique silently breaks if you reach for `reversed()`: the flip destroys the tie order the earlier pass established, and every field you had already ordered is scrambled within each tie group. It also matters at a cutoff. Rank a geocoding batch by confidence and keep the candidates above a 92nd-percentile threshold: if several candidates sit exactly on the boundary value, which ones land inside a `[:k]` slice is decided entirely by tie order. Swap `reverse=True` for a `reversed()` call, or reorder the passes, and the slice takes a different record at the boundary — a one-record off-by-one that is invisible in tests with distinct scores and shows up only on real data where ties are common. The fix is to make the ordering total: add an explicit, unique tie-breaker (a stable identifier) as the last component of the key, so the result does not depend on input order at all. ### How to answer it Say the three things in order: `reverse=` is all-or-nothing; a tuple key with a negated numeric component handles the common case in one pass; stability plus least-significant-first passes handles the general case, including descending on a non-numeric field. Then volunteer that ties at a cutoff deserve an explicit tie-breaker rather than reliance on stability, and you have answered the follow-up before it is asked.

  • Is sorted(data, key=f, reverse=True) the same as reversing an ascending sort?
    No. `reverse=True` is defined to preserve the original relative order of equal elements, so stability holds in the descending direction too. `reversed(sorted(...))` flips ties along with everything else, which silently destroys the ordering an earlier pass established and changes which record sits at a cutoff boundary.
  • When does the negated-number tuple key stop working, and what do you do instead?
    The moment the descending field is not numeric: `str`, `date` and `tuple` have no negation. A date can be converted to a numeric timestamp and negated; a string cannot, and character-reversal tricks are wrong outside fixed-length ASCII. Fall back to two stable passes, or wrap the value in a small class defining the reversed ordering.
  • Two candidates tie exactly on the score you slice at — how do you make the result deterministic?
    Do not rely on stability to decide it. Make the ordering total by appending a unique tie-breaker as the last component of the key — a record id, say — so the output no longer depends on input order. Ties at a cutoff are precisely where a one-record off-by-one hides, and tests with distinct values never catch it.
  • What does the multi-pass technique cost compared with one tuple key?
    k passes are k full sorts: k times the comparison work and a fresh list per pass, though each key is simpler. For a few hundred thousand rows that is usually irrelevant next to readability; on a hot path, prefer the single tuple key whenever every descending field is numeric.

It is like filing index cards alphabetically by name, then re-filing the whole stack by score without disturbing the cards inside each score group — the alphabetical order survives underneath.

saying these in an interview costs you the question

  • Thinks reverse= can be applied per field or per tuple component
  • Tries to negate a string or a date to sort it descending
  • Assumes reversed(sorted(x)) equals sorted(x, reverse=True)
  • Sorts by the most significant field first in a multi-pass sort
  • Believes Python's sort is not guaranteed stable
  • Leaves ties at a cutoff to be decided by input order

context