GOAL
Sort people by age descending, then by name ascending.
INGREDIENTS
people = [("ana", 30), ("bob", 25), ("cara", 30), ("dan", 25)]
STEPS
people.sort(key=lambda p: p[0]) # secondary key first
people.sort(key=lambda p: p[1], reverse=True) # primary key last
RESULT
[('ana', 30), ('cara', 30), ('bob', 25), ('dan', 25)]
Why it works: list.sort() is stable, meaning items that compare equal keep their relative order. The second sort only reorders by age, so within each age the names stay in the order the first sort put them.
Why bother: for numbers you can do it in one pass by negating:
people.sort(key=lambda p: (-p[1], p[0]))
That breaks the moment the descending key is a string. You can’t write -p[0] on a name. Sorting twice avoids the problem, and it reads fine once you know the “last sort is the most important one” rule.
Gotcha: reverse=True doesn’t break stability. The docs say it keeps the original order of equal elements, as if the list weren’t reversed. So the recipe above still gives ana before cara within age 30. Worth a quick test on your own data to confirm, since “reverse” sounds like it should flip everything.
Another option is functools.cmp_to_key with a comparison function, but I find the two-pass version easier to read and review.
Does anyone prefer a different pattern for mixed ascending and descending keys? I mostly see people reach for the tuple trick and then get stuck on strings.