Learning Goals
3 minBy the end of this lesson you can:
- Pick between
sorted()(returns new list) andlist.sort()(in-place). - Use the
key=parameter with a lambda or a named function. - Sort by multiple criteria with a tuple key — ascending and descending mixed.
- Recognise Timsort's name and why Python uses it.
Warm-Up · Two Names, Two Behaviours
5 minnums = [3, 1, 4, 1, 5, 9, 2, 6] # sorted() — returns a NEW list. Original untouched. result = sorted(nums) print(result) # [1, 1, 2, 3, 4, 5, 6, 9] print(nums) # [3, 1, 4, 1, 5, 9, 2, 6] — unchanged # list.sort() — sorts IN PLACE. Original mutated. nums.sort() print(nums) # [1, 1, 2, 3, 4, 5, 6, 9] — now sorted # (sort returns None, so don't write x = nums.sort())
sorted for "give me a new sorted version". .sort for "sort this list in place".
Python's built-in sort is Timsort — an adaptive hybrid invented by Tim Peters in 2002. It's O(n log n), stable, and near-linear on partially sorted data. You will never out-write it.
New Concept · key= and reverse=
14 minBasic sort
# Plain ascending print(sorted([5, 2, 8, 1])) # [1, 2, 5, 8] print(sorted(["banana", "apple", "cherry"])) # alphabetical # Descending print(sorted([5, 2, 8, 1], reverse=True)) # [8, 5, 2, 1]
Sort by attribute or field — key=
students = [ {"name": "Aisyah", "score": 92}, {"name": "Wei Jie", "score": 87}, {"name": "Priya", "score": 95}, ] # By score ascending sorted(students, key=lambda s: s["score"]) # By score descending sorted(students, key=lambda s: s["score"], reverse=True) # By name alphabetically sorted(students, key=lambda s: s["name"])
You met this pattern back in PY-L2-04 and again in PY-L3-20. Today we put it on a pedestal: every Python sort uses key=.
Multi-key sort with tuples
# Sort by score desc, then by name asc as tie-breaker sorted(students, key=lambda s: (-s["score"], s["name"]))
Tuples compare element-by-element. (-score, name) sorts first by negated score (so higher = lower number = first), then by name. Mixed direction in one tuple — use the negation trick.
What if you can't negate?
For strings or other non-numeric keys, do two sorts — first the secondary, then the primary. Python's sort is stable, so equal-primary items keep the secondary's order.
# Sort by department asc, then name desc within department sorted_by_name_desc = sorted(staff, key=lambda s: s["name"], reverse=True) result = sorted(sorted_by_name_desc, key=lambda s: s["department"])
Two passes; Timsort is fast enough that this is fine.
The operator.itemgetter / attrgetter shortcut
For simple key functions, operator module is slightly faster than a lambda:
from operator import itemgetter, attrgetter # Same as key=lambda s: s["score"] sorted(students, key=itemgetter("score")) # Same as key=lambda h: h.hp sorted(heroes, key=attrgetter("hp")) # Multi-key sorted(students, key=itemgetter("score", "name")) # tuple key
Reads slightly cleaner, runs slightly faster. Common in code-review style guides.
Sort strings as numbers
prices = ["12.50", "3.00", "100.25", "7.99"] sorted(prices) # ['100.25', '12.50', '3.00', '7.99'] -- LEX! sorted(prices, key=float) # ['3.00', '7.99', '12.50', '100.25']
Strings compare lexicographically by default. key=float converts each to a number for comparison.
Why Timsort?
Timsort:
- Finds existing "runs" (already-sorted sub-sequences) in the data.
- For each run, uses insertion sort (the "small-list winner" from PY-L3-40).
- Merges runs using mergesort.
- Worst case O(n log n). Best case (already sorted) O(n).
- Stable.
Designed for real-world data — which is usually somewhat sorted. Python adopted it in 2002; Java did in 2009. It's arguably the most-used sorting algorithm in the world.
Worked Example · The Sort Showcase
12 minSave as sort_showcase.py:
# sort_showcase.py — every common sort use case from operator import itemgetter people = [ {"name": "Aisyah", "age": 13, "score": 92}, {"name": "Wei Jie", "age": 12, "score": 87}, {"name": "Priya", "age": 13, "score": 95}, {"name": "Iman", "age": 12, "score": 92}, # ties with Aisyah on score {"name": "Aizat", "age": 12, "score": 78}, ] # 1 — plain alpha by name for p in sorted(people, key=itemgetter("name")): print(f" {p['name']:<10} age {p['age']} score {p['score']}") print() # 2 — score descending for p in sorted(people, key=itemgetter("score"), reverse=True): print(f" {p['score']} {p['name']}") print() # 3 — multi-key: score desc, then name asc as tiebreaker for p in sorted(people, key=lambda p: (-p["score"], p["name"])): print(f" {p['score']} {p['name']}") print() # 4 — group by age, sort each group by score groups = {} for p in people: groups.setdefault(p["age"], []).append(p) for age in sorted(groups): print(f"Age {age}:") for p in sorted(groups[age], key=lambda p: -p["score"]): print(f" {p['name']:<10} {p['score']}")
Output (showcasing each shape)
# By name
Aisyah age 13 score 92
Aizat age 12 score 78
Iman age 12 score 92
Priya age 13 score 95
Wei Jie age 12 score 87
# By score desc
95 Priya
92 Aisyah
92 Iman
87 Wei Jie
78 Aizat
# Multi-key
95 Priya
92 Aisyah # comes before Iman because name alphabetises first
92 Iman
87 Wei Jie
78 Aizat
# Grouped by age
Age 12:
Iman 92
Wei Jie 87
Aizat 78
Age 13:
Priya 95
Aisyah 92Read the diff
Four sorts, four different criteria. The multi-key version handles the tie between Aisyah and Iman by falling through to name. The grouped version uses the group-by recipe from PY-L2-12 plus a per-group sort. All built on the same sorted + key=.
Try It Yourself
13 minSort ["python", "is", "fun", "indeed"] by length ascending. Then descending.
Hint
words = ["python", "is", "fun", "indeed"] print(sorted(words, key=len)) # ['is', 'fun', 'python', 'indeed'] print(sorted(words, key=len, reverse=True)) # ['indeed', 'python', 'fun', 'is']
len is callable on a string — pass it directly as the key. No lambda needed.
Sort ["100", "5", "42", "9"] numerically (not lexicographically).
Hint
nums_as_str = ["100", "5", "42", "9"] print(sorted(nums_as_str)) # ['100', '42', '5', '9'] -- lex print(sorted(nums_as_str, key=int)) # ['5', '9', '42', '100']
key=int converts each to an int for comparison. The output keeps the original strings.
Sort [-3, 1, -2, 2, 0, -1] so the smaller absolute value comes first; if same absolute value, the negative one comes first.
Hint
nums = [-3, 1, -2, 2, 0, -1] result = sorted(nums, key=lambda n: (abs(n), n)) print(result) # [0, -1, 1, -2, 2, -3]
Tuple key: (abs, signed). Same abs → smaller signed (the negative) comes first.
Mini-Challenge · Multi-Criterion Sort UI
8 minBuild sort_ui.py. Given a list of dicts, accept a sort spec from the user (a string like "score desc, name asc") and produce the sorted list. Support multiple criteria.
Show one possible solution
# sort_ui.py — parse a sort spec into a key function def build_key_func(spec): """Parse 'field1 dir1, field2 dir2' into a key function.""" parts = [] for clause in spec.split(","): bits = clause.strip().split() field = bits[0] descending = len(bits) > 1 and bits[1].lower().startswith("desc") parts.append((field, descending)) def key_func(item): result = [] for field, descending in parts: v = item[field] if descending and isinstance(v, (int, float)): v = -v result.append(v) return tuple(result) # Note: descending strings require a separate two-pass sort, not handled here return key_func people = [ {"name": "Aisyah", "age": 13, "score": 92}, {"name": "Wei Jie", "age": 12, "score": 87}, {"name": "Priya", "age": 13, "score": 95}, {"name": "Iman", "age": 12, "score": 92}, ] while True: spec = input("Sort spec (e.g. 'score desc, name asc'): ") if not spec: break try: sorted_people = sorted(people, key=build_key_func(spec)) for p in sorted_people: print(f" {p}") except (KeyError, ValueError) as e: print(f" ! Bad spec: {e}")
Non-negotiables: parse a comma-separated spec, build a tuple key, support asc and desc on numeric fields. Pretty close to what a real sortable-table UI needs to support multi-column sorting.
Recap
3 minsorted(lst, key=, reverse=) for new sorted list; lst.sort() for in-place. The key argument extracts what to compare. Tuple keys + negation trick = multi-key sort with mixed directions. operator.itemgetter/attrgetter are slightly faster than lambdas for simple keys. Python uses Timsort — adaptive, stable, O(n log n). You will not beat it. Don't try.
Vocabulary Card
- sorted(iter, key=, reverse=)
- Returns a new sorted list. Works on any iterable.
- list.sort(key=, reverse=)
- Sorts the list in place. Returns None — don't assign the result.
- key function
- Extracts the value used for comparison. Called once per item.
- stable sort
- Equal items keep their original relative order. Timsort is stable.
- Timsort
- Python's built-in sort algorithm. Hybrid mergesort + insertion sort. O(n log n).
Homework
4 minBuild file_sort.py. Given a list of file dicts with name, size_bytes, mtime (timestamp), produce four reports:
- By size, largest first.
- By name, alphabetical.
- By extension (.docx, .jpg, .txt), then by name within each.
- By modification time, newest first; ties broken by smallest first.
Sample · file_sort.py
files = [ {"name": "report.docx", "size_bytes": 124_000, "mtime": 1716800000}, {"name": "photo.jpg", "size_bytes": 2_500_000, "mtime": 1716700000}, {"name": "notes.txt", "size_bytes": 400, "mtime": 1716750000}, {"name": "memo.docx", "size_bytes": 10_000, "mtime": 1716700000}, {"name": "icon.jpg", "size_bytes": 800_000, "mtime": 1716900000}, ] # 1 — by size desc for f in sorted(files, key=lambda x: x["size_bytes"], reverse=True): print(f" {f['name']:<14} {f['size_bytes']:>10}") print() # 2 — alpha for f in sorted(files, key=lambda x: x["name"]): print(f" {f['name']}") print() # 3 — by extension then name def ext(f): return f["name"].rsplit(".", 1)[-1].lower() for f in sorted(files, key=lambda x: (ext(x), x["name"])): print(f" {f['name']:<14} [{ext(f)}]") print() # 4 — newest first, smallest tie-break for f in sorted(files, key=lambda x: (-x["mtime"], x["size_bytes"])): print(f" {f['name']:<14} mtime {f['mtime']} size {f['size_bytes']}")
Non-negotiables: four sorts, each one line. Multi-key sorts use tuples. Negation trick for descending-numeric. Helper function for extension extraction.