Sorting in Python: sort() vs sorted() vs key=
sort() changes the list and returns None. sorted() returns a new list. The key argument is what makes both useful.
Python has two ways to sort, and picking the wrong one is the most common mistake in this area:
nums = [3, 1, 2]
nums.sort() # returns None, nums is now [1, 2, 3]
sorted(nums) # returns [1, 2, 3], nums is unchanged
list.sort() changes the list in place and gives back None. sorted() leaves the original alone and gives back a new list.
Every result on this page was run in Python 3.13 through Pyodide 0.28.3.
The mistake this causes
ordered = nums.sort()
# ordered is None
The list did get sorted. The variable just holds nothing, because sort() returns None by design: it is a Python convention that methods changing an object in place return nothing, so you cannot mistake them for methods that produce a new object.
The next line then fails with 'NoneType' object is not subscriptable or similar, several steps away from the real mistake.
Two correct forms:
nums.sort() # then keep using nums
ordered = sorted(nums) # then use ordered, nums untouched
Choose by whether you need the original. If nothing else uses it, sort() is slightly faster because it does not build a copy. If anything else might, sorted() is the safe answer.
sorted() also works on any iterable, not just lists, and always hands back a list:
sorted("cba") # ['a', 'b', 'c']
sorted({"b": 1, "a": 2}) # ['a', 'b'] (the keys)
key= is where the power is
Both take a key function, called once per item, whose return value is used for the comparison. The items themselves are what end up in the result.
sorted(["ccc", "a", "bb"], key=len)
# ['a', 'bb', 'ccc']
For a list of dictionaries or objects, key is how you choose the field:
people = [
{"name": "Ada", "age": 36},
{"name": "Alan", "age": 36},
{"name": "Grace", "age": 45},
]
sorted(people, key=lambda p: p["age"])
# Ada, Alan, Grace
operator.itemgetter("age") does the same thing and is marginally faster, which matters only on large lists.
The important part: key receives one item and returns the value to sort by. It is not a comparison function, and it is called once per item rather than once per comparison, which is why it is both simpler and quicker than the old cmp approach.
Case sensitivity is the usual surprise
The default string ordering is by character code, and every capital letter comes before every lowercase one:
sorted(["apple", "Banana", "cherry"])
# ['Banana', 'apple', 'cherry']
"Banana" first, because B is 66 and a is 97. That is rarely what a person means by alphabetical.
sorted(["apple", "Banana", "cherry"], key=str.lower)
# ['apple', 'Banana', 'cherry']
Passing str.lower as the key sorts on the lowercased form while keeping the original strings in the output. This is the single most useful application of key.
Two related surprises worth knowing:
sorted(["zebra", "éclair", "apple"])
# ['apple', 'zebra', 'éclair']
Accented characters sort after z, because their character codes are higher. For genuinely human alphabetical order in a language with accents, you need locale.strxfrm or a library; the built-in comparison is not linguistic.
sorted(["item10", "item9"])
# ['item10', 'item9']
"item10" before "item9", because 1 comes before 9 as a character. Numbers inside strings do not sort numerically. The fix is a key that extracts the number.
Stability, and why it makes multi-key sorting work
Python's sort is stable: items that compare equal keep their original relative order.
sorted(people, key=lambda p: p["age"])
# Ada, Alan, Grace
Ada and Alan both have age 36, and Ada was first in the input, so Ada stays first. That is guaranteed, not incidental.
Stability gives you multi-key sorting for free by sorting twice, least important key first:
people.sort(key=lambda p: p["name"]) # then
people.sort(key=lambda p: p["age"]) # primary key last
The second sort preserves the name ordering within each age group. This reads oddly the first time and it is the standard technique.
The alternative is one sort with a tuple key, which is usually clearer:
sorted(people, key=lambda p: (p["age"], p["name"]))
Tuples compare element by element, so this sorts by age, then by name within each age.
Descending, and mixing directions
sorted([3, 1, 2], reverse=True) # [3, 2, 1]
reverse=True flips the whole ordering, and it combines with key:
sorted(["apple", "Banana"], key=str.lower, reverse=True)
# ['Banana', 'apple']
Mixing directions across two keys is the case reverse cannot express, since it applies to everything. For numbers, negate the value in the key:
sorted([("b", 2), ("a", 2), ("c", 1)], key=lambda t: (-t[1], t[0]))
# [('a', 2), ('b', 2), ('c', 1)]
Descending by the number, ascending by the letter. Negation only works on numbers, so for strings you fall back to two stable passes.
Sorting mixed types raises
sorted([1, "a"])
# TypeError: '<' not supported between instances of 'str' and 'int'
Python will not guess an ordering between a number and a string. This usually means your data is not the shape you assumed, and the useful response is to look at where the odd item came from rather than to force a key that stringifies everything.
None mixed into numbers raises the same way, which is a common outcome of a missing field. A key with a default handles it deliberately:
sorted(rows, key=lambda r: r.get("age") or 0)
Sorting a dictionary
Dictionaries do not sort in place, so this is always sorted(), and what you get back depends on what you feed it.
Sorting the keys is the default, because iterating a dictionary gives keys:
scores = {"ada": 3, "grace": 9, "alan": 5}
sorted(scores) # ['ada', 'alan', 'grace']
Sorting the keys by their values uses the dictionary's own get as the key function:
sorted(scores, key=scores.get) # ['ada', 'alan', 'grace']
For pairs, sort .items() and pick the element to sort on:
sorted(scores.items(), key=lambda kv: kv[1], reverse=True)
# [('grace', 9), ('alan', 5), ('ada', 3)]
kv is a (key, value) tuple, so kv[1] is the value. To end up with a dictionary again, rebuild one, which works because dictionaries have preserved insertion order since Python 3.7:
dict(sorted(scores.items(), key=lambda kv: kv[1]))
# {'ada': 3, 'alan': 5, 'grace': 9}
sorted(reverse=True) is not reversed()
Two similar-looking operations that do different things:
sorted([1, 3, 2], reverse=True) # [3, 2, 1]
list(reversed([1, 3, 2])) # [2, 3, 1]
sorted(reverse=True) sorts, then presents the result descending. reversed() does not sort at all, it just walks the existing order backwards.
Use reversed() when the list is already in the order you want and you need it the other way round, such as showing newest-first from a list appended in time order. Use reverse=True when you want a descending sort. Mixing them up produces output that looks shuffled rather than wrong, which makes it slow to spot.
reversed() also returns an iterator rather than a list, so it needs list() if you want to keep it.
key works on more than sorting
The same argument appears on min and max:
max(["aa", "b", "ccc"], key=len) # 'ccc'
Worth knowing because "find the longest" or "find the most recent" does not need a sort at all. Sorting to take one item is doing far more work than the question requires.
Sorting objects and custom classes
For your own classes, a key works the same way as for dictionaries:
sorted(users, key=lambda u: u.age)
operator.attrgetter("age") is the equivalent of itemgetter for attributes, and it takes several names to build a tuple key in one step:
from operator import attrgetter
sorted(users, key=attrgetter("age", "name"))
If a class has a natural ordering, defining __lt__ lets sorted work with no key at all:
class Version:
def __lt__(self, other):
return self.parts < other.parts
Worth doing only when the ordering is genuinely intrinsic to the type, such as versions or dates. For anything where different callers might want different orderings, a key at the call site is clearer, because it puts the choice where the decision is made.
Three common mistakes
Assigning the result of sort(). ordered = nums.sort() gives you None. Use sorted() when you want a value back.
Passing a comparison function instead of a key. Python 2 had cmp; Python 3 does not. A key function takes one item and returns the value to sort by. If you genuinely need pairwise comparison logic, functools.cmp_to_key wraps it, but a key function is almost always expressible and is faster because it runs once per item rather than once per comparison.
Sorting when you only need one item. sorted(items)[0] sorts everything to take the smallest. min(items) does not, and both min and max accept the same key argument.
How fast is it
Python's sort is Timsort, which is designed around the observation that real data is often partly ordered already. On randomly ordered input it does the expected amount of work; on input that is already sorted, or sorted in runs, it is substantially faster because it detects and merges those runs rather than starting from scratch.
Two practical consequences. Re-sorting an already-sorted list is cheap, so defensively sorting is rarely the performance problem people expect. And sorting a list you just built in order costs very little, which makes "build then sort" a reasonable pattern rather than something to optimise away.
Where cost does appear is in the key function, which runs once per item. A key that parses a date string or does a lookup is doing that work for every element, and on a large list it dominates. Computing the key once into a tuple, sorting that, then extracting, is the fix, and it is only worth doing when profiling says so.
Quick reference
lst.sort() # in place, returns None
sorted(lst) # new list, original untouched
sorted(lst, reverse=True) # descending
sorted(lst, key=str.lower) # case-insensitive
sorted(lst, key=len) # by length
sorted(rows, key=lambda r: r["age"]) # by field
sorted(rows, key=lambda r: (r["age"], r["name"])) # two keys
max(lst, key=len) # no sort needed
Want to sort real data and see the edge cases immediately? Start with the Python track, or read list.pop() for the other in-place method that returns something unexpected.
More from the blog
zip() in Python: loop over two lists at once
zip pairs items by position and stops at the shortest input, discarding the rest silently. strict=True is the fix.
Read morerange() in Python: start, stop, step, and why it is lazy
range(1000000) uses 24 bytes. A list of a thousand numbers uses 4,028. Here is what range actually is, measured.
Read moreReady to write some code?
Put this into practice - start your first free lesson. No setup, no credit card.