Python's sort is fast (Timsort), stable, and customised with a key function — rarely with a comparator. Master key= and most ordering problems become one line.
sorted() vs list.sort()
nums = [5, 2, 9, 1]
print(sorted(nums), nums) # [1, 2, 5, 9] [5, 2, 9, 1]
result = nums.sort(reverse=True) # in place
print(result, nums) # None [9, 5, 2, 1]
print(sorted("hello")) # ['e', 'h', 'l', 'l', 'o']
print(sorted({"b": 1, "a": 2})) # ['a', 'b'] ← iterating a dict gives keyssorted(iterable) | lst.sort() | |
|---|---|---|
| Works on | Any iterable (str, dict, set, generator…) | Lists only |
| Returns | A new list | None — sorts in place |
| Memory | Copies the data | No extra list |
| Options | key=, reverse= | key=, reverse= |
The key function
key is called once per item; items are ordered by those computed keys. Any callable works: a built-in, a method, a lambda, an operator helper.
words = ["banana", "Apple", "fig", "cherry"]
print(sorted(words)) # ['Apple', 'banana', 'cherry', 'fig']
print(sorted(words, key=str.lower)) # ['Apple', 'banana', 'cherry', 'fig']
print(sorted(words, key=len)) # ['fig', 'Apple', 'banana', 'cherry']
print(sorted(words, key=lambda w: w[-1])) # ['banana', 'Apple', 'fig', 'cherry']
print(sorted([-4, 1, -2, 3], key=abs)) # [1, -2, 3, -4]Multi-level ordering
Return a tuple from the key: ties on the first element fall through to the second. Negate numbers to flip one level to descending.
students = [("asha", 90, 21), ("ravi", 85, 22), ("kiran", 90, 20), ("meera", 85, 20)]
# marks descending, then name ascending
print([s[0] for s in sorted(students, key=lambda s: (-s[1], s[0]))])
# ['asha', 'kiran', 'meera', 'ravi']['asha', 'kiran', 'meera', 'ravi']Can't negate a string? Use stability: sort by the secondary key first, then by the primary key. Each pass keeps the previous order among ties.
people = [("asha", "dev"), ("ravi", "ops"), ("kiran", "dev"), ("meera", "ops")]
by_name_desc = sorted(people, key=lambda p: p[0], reverse=True) # secondary first
result = sorted(by_name_desc, key=lambda p: p[1]) # then primary
print(result) # [('kiran', 'dev'), ('asha', 'dev'), ('ravi', 'ops'), ('meera', 'ops')]operator.itemgetter & attrgetter
from operator import itemgetter, attrgetter
from dataclasses import dataclass
rows = [{"name": "ravi", "age": 30}, {"name": "asha", "age": 25}]
print([r["name"] for r in sorted(rows, key=itemgetter("age"))]) # ['asha', 'ravi']
pairs = [(2, "b"), (1, "z"), (2, "a")]
print(sorted(pairs, key=itemgetter(0, 1))) # [(1, 'z'), (2, 'a'), (2, 'b')]
@dataclass
class Emp:
name: str
salary: int
staff = [Emp("a", 50), Emp("b", 70), Emp("c", 60)]
print([e.name for e in sorted(staff, key=attrgetter("salary"), reverse=True)]) # ['b', 'c', 'a']Sorting dict items
scores = {"asha": 91, "ravi": 78, "kiran": 91, "zoe": 85}
top = sorted(scores.items(), key=lambda kv: (-kv[1], kv[0]))
print(top[:3]) # [('asha', 91), ('kiran', 91), ('zoe', 85)]
print(dict(sorted(scores.items()))) # {'asha': 91, 'kiran': 91, 'ravi': 78, 'zoe': 85}
print(max(scores, key=scores.get)) # asha ← first max winsComparators with cmp_to_key
When the order can't be expressed as a key — it depends on both items together — write a comparator returning negative / zero / positive and wrap it with functools.cmp_to_key.
from functools import cmp_to_key
def largest_number(nums):
"""Arrange numbers to form the largest possible number."""
strs = [str(n) for n in nums]
def cmp(a, b):
if a + b > b + a:
return -1 # a goes first
if a + b < b + a:
return 1
return 0
strs.sort(key=cmp_to_key(cmp))
result = "".join(strs)
return "0" if result[0] == "0" else result
print(largest_number([3, 30, 34, 5, 9])) # 9534330
print(largest_number([0, 0])) # 0Natural sort & custom objects
import re
files = ["file10.txt", "file2.txt", "file1.txt"]
print(sorted(files)) # ['file1.txt', 'file10.txt', 'file2.txt']
def natural_key(s):
return [int(t) if t.isdigit() else t.lower() for t in re.split(r"(\d+)", s)]
print(sorted(files, key=natural_key)) # ['file1.txt', 'file2.txt', 'file10.txt']
class Task:
def __init__(self, pri, name):
self.pri, self.name = pri, name
def __lt__(self, other): # sort() only needs <
return self.pri < other.pri
def __repr__(self):
return self.name
print(sorted([Task(3, "c"), Task(1, "a"), Task(2, "b")])) # [a, b, c]min, max, heapq & bisect take a key too
import heapq, bisect
words = ["kiwi", "banana", "fig", "apple"]
print(min(words, key=len), max(words, key=len)) # fig banana
print(heapq.nsmallest(2, words, key=len)) # ['fig', 'kiwi']
print(max([], default="empty")) # empty
records = [(1, "a"), (4, "b"), (9, "c")] # sorted by first field
print(bisect.bisect_left(records, 5, key=lambda r: r[0])) # 2 ← key= needs 3.10+| Need | Best tool | Cost |
|---|---|---|
| Full order | sorted / .sort | O(n log n) |
| Just the min or max | min / max | O(n) |
| Top k (k small) | heapq.nsmallest / nlargest | O(n log k) |
| Keep a list sorted while inserting | bisect.insort | O(log n) search + O(n) insert |
Timsort facts & pitfalls
try:
sorted([3, "a", 1])
except TypeError as e:
print(e) # '<' not supported between instances of 'str' and 'int'
print(sorted([3, None, 1], key=lambda x: (x is None, x or 0))) # [1, 3, None]How do you sort by one field descending and another ascending?
Is Python's sort stable, and why does it matter?
When would you use cmp_to_key?