Sorting by something other than the value itself
sorted takes a function that says what to compare
words = ["banana", "fig", "apple"]
sorted(words) # ['apple', 'banana', 'fig'] — alphabetical
sorted(words, key=len) # ['fig', 'apple', 'banana'] — by length
sorted(words, reverse=True) # reverse alphabeticalkey is a function applied to each element; the results are what get compared. Note that len is passed without brackets — you are handing over the function itself, not calling it. key=len() is an immediate TypeError, and it is the most common mistake here.
For anything not already a function, write a lambda — a small unnamed function:
people = [{"name": "Asha", "age": 34}, {"name": "Ravi", "age": 28}]
sorted(people, key=lambda p: p["age"])Read lambda p: p["age"] as "given p, produce p's age".
For the common cases the standard library has faster, clearer versions:
from operator import itemgetter, attrgetter
sorted(people, key=itemgetter("age"))
sorted(objects, key=attrgetter("created_at"))Sorting by two things at once
Return a tuple. Tuples compare element by element, left first:
sorted(people, key=lambda p: (p["city"], p["age"]))City ascending; within each city, age ascending. To reverse only one of them, negate a number:
sorted(people, key=lambda p: (p["city"], -p["age"]))reverse=True reverses everything, which is usually not what a report wants.
For a non-numeric field you cannot negate, use the other route: Python's sort is stable, meaning equal elements keep their existing relative order. So you can sort twice, least important key first:
people.sort(key=itemgetter("name")) # tie-break
people.sort(key=itemgetter("city"), reverse=True) # primaryStability is a guarantee, not an implementation accident, and it is the reason this works.
sort against sorted, again
list.sort() orders the list in place and returns None. sorted(anything) returns a new list and works on any iterable — a set, a dictionary's items, a generator, a file. Sorting a dictionary by value:
top = sorted(counts.items(), key=itemgetter(1), reverse=True)[:10]Case, and why sorted looks wrong on names
sorted(["banana", "Apple", "cherry"])
# ['Apple', 'banana', 'cherry']Every capital letter sorts before every lowercase letter, because the comparison is on the underlying code points and A is 65 while a is 97. For a human-facing list:
sorted(names, key=str.lower)str.casefold is stricter still and handles cases like the German double-s properly.
The limitation worth stating plainly
Python sorts strings by Unicode code point. That is not alphabetical order in most of the world's languages.
- Accented characters land after all unaccented ones, so
zebrasorts beforeÅngström. - Devanagari, Tamil and Bengali text sorts by code point, which is not the order any dictionary in those languages uses.
- Sorting mixed English and Indian-language names gives an order no reader recognises.
The standard library's locale.strxfrm can do proper collation, but only if the operating system has that locale installed, which on a container or a phone it usually does not. The reliable route is the free PyICU package, which carries the Unicode collation data itself, or pyuca. If your output is going in front of readers in a language with its own alphabet, code-point order is a bug, and it is one nobody reports because it looks merely arbitrary.
Numbers hidden in strings
sorted(["file10.txt", "file2.txt"])
# ['file10.txt', 'file2.txt']Character by character, 1 precedes 2, so file10 comes first. This is correct string ordering and wrong for a person. The fix is a key that splits digits from text:
import re
def natural(s):
return [int(p) if p.isdigit() else p for p in re.split(r"(\d+)", s)]
sorted(["file10.txt", "file2.txt"], key=natural)Sorting mixed types fails
sorted([3, "1", 2])
# TypeError: '<' not supported between instances of 'str' and 'int'Python 3 refuses rather than inventing an order. This is almost always a message that a column read from a file was never converted, and it is better to be told now than to get a silently strange order.
Sorting is not the same as taking the top few
If you only want the largest ten of a million items, sorting the million is wasted work:
import heapq
top = heapq.nlargest(10, rows, key=itemgetter("score"))nlargest keeps a heap of ten and scans once, which is roughly n log k rather than n log n. On a million rows that is a few hundred milliseconds saved, and the code says what it means.
What it costs
Python's sort is Timsort: worst case n log n, and much faster on data that is already partly ordered, which real data usually is. The key function is called exactly once per element, not once per comparison, so an expensive key is affordable.
The one thing to keep
`key` takes a function applied once per element, tuples give multi-level ordering, and stable sorting lets you order by several fields by sorting repeatedly from the least important key upwards.
Before you move on
A leaderboard must show highest score first, and for equal scores the name alphabetically. `sorted(rows, key=lambda r: (r["score"], r["name"]), reverse=True)` gives the right score order but reversed names within a tie. What is the cleanest correct fix?
Pick the one you would defend. Nobody sees your answer.