Devpuff
Pricing
Log InStart Free
  1. Home
  2. Blog
  3. Sorting in Python: sort() vs sorted() vs key=
Learn

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.

By Max Arthur
Co-Founder & Content Marketer·October 13, 2026·9 Min read
White text reading 'How to sort a list in Python' on a dark purple dotted background

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.

Keep reading

More from the blog

White text reading 'How zip() works in Python' on a dark purple dotted background
October 14, 2026·8 min readLearn

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 more
White text reading 'How range() works in Python' on a dark purple dotted background
October 12, 2026·8 min readLearn

range() 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 more
{ }
✦

Ready to write some code?

Put this into practice - start your first free lesson. No setup, no credit card.

Start learning free
Devpuff

Learn to code by doing. One tiny, playful lesson at a time.

Learn
ProgramsCoursesPricing
Company
AboutBlogResourcesAffiliates
Support
Help CenterContactStatus
Programs
Frontend DevelopmentFull-Stack DevelopmentReact DeveloperPython Developer
Courses
Advanced ReactAsync JavaScriptAsync PythonCSS BasicsCSS LayoutDSA Basics
Learn to Code
Learn JavaScriptLearn PythonLearn SQLLearn HTMLLearn CSSLearn ReactBrowse All Topics
Platform Comparisons
Devpuff vs CodecademyDevpuff vs Mimo
© 2026 Devpuff. All rights reserved.Privacy PolicyTerms and ConditionsCookies PolicyRefund Policy
Devpuff vs Sololearn
Devpuff vs freeCodeCamp
Read All Comparisons