How to Sort a List of Objects by Attribute in Python
To sort a list of objects by an attribute in Python, use sorted(items, key=lambda x: x.score, reverse=True) or items.sort(...) with the same arguments. Python's built-in sort is O(n log n) and stable. A hand-written bubble sort - two nested loops that swap neighbours - is O(n^2) and becomes very slow once the list has a few thousand items.
The slow version
This ranking function sorts users by score, highest first:
from dataclasses import dataclass
@dataclass
class User:
id: int
name: str
score: float
def rank_users(users: list[User]) -> list[User]:
for i in range(len(users)):
for j in range(len(users) - 1):
if users[j].score < users[j + 1].score:
users[j], users[j + 1] = users[j + 1], users[j]
return users
This is a bubble sort. It gives the right answer, and with 50 users nobody notices how slow it is. With a few thousand users, a search request starts to hang.
Why it is slow
The inner loop walks the whole list, and the outer loop repeats that once per
user. For n users that is about n * n comparisons:
| Users | Comparisons (n x n) |
|---|---|
| 1,000 | 1,000,000 |
| 5,000 | 25,000,000 |
| 10,000 | 100,000,000 |
Python's built-in sort (Timsort) needs about n log n comparisons - for
10,000 items that is in the order of 130,000 instead of 100,000,000. It is
also written in C, so each comparison is cheaper too.
The fix: sorted() with a key
def rank_users(users: list[User]) -> list[User]:
return sorted(users, key=lambda u: u.score, reverse=True)
keyis a function that returns the value to sort by. Python calls it once per item, not once per comparison.reverse=Truegives descending order, highest score first.sorted()returns a new list and leaves the input unchanged. Useusers.sort(key=..., reverse=True)instead if you want to sort the list in place and do not need the original order.
Note that the old bubble sort changed the caller's list in place. If other
code depended on that, use users.sort(...) and return users.
operator.attrgetter
The operator module has a ready-made key function that is a little faster
than a lambda and easy to read:
from operator import attrgetter
ranked = sorted(users, key=attrgetter("score"), reverse=True)
For dictionaries, use itemgetter("score") in the same way.
Sorting by more than one field
Return a tuple from the key. Python compares the first element, then the second if the first is equal:
# highest score first, then name A-Z for equal scores
ranked = sorted(users, key=lambda u: (-u.score, u.name))
The minus sign reverses a number field without reverse=True, so the name
can still sort A to Z. For fields you cannot negate, such as strings, sort in
two passes - Python's sort is stable, so a second sort keeps the order of the
first for equal keys:
ranked = sorted(users, key=attrgetter("name")) # secondary
ranked = sorted(ranked, key=attrgetter("score"), reverse=True) # primary
What "stable" means
A stable sort keeps items with equal keys in their original order. If two
users both have a score of 0.9, they stay in the order they had in the input.
Python's sort is stable, including with reverse=True. This makes results
predictable, which matters for tests and for pages where the order should not
change between requests.
Measure before and after
time python3 -c "import random; from search import User, rank_users; rank_users([User(i, 'u', random.random()) for i in range(3000)])"
The bubble sort takes seconds; sorted() finishes almost at once. Then check
the result, not only the speed:
import random
users = [User(i, "u", random.random()) for i in range(5000)]
ranked = rank_users(users)
assert all(ranked[i].score >= ranked[i + 1].score for i in range(len(ranked) - 1))
assert rank_users([]) == []
When you only need the top few
If a page shows only the top 10 of a large list, heapq.nlargest avoids
sorting everything:
import heapq
top10 = heapq.nlargest(10, users, key=attrgetter("score"))
The wider lesson
Hand-written sorting loops in application code are almost always a mistake.
When you review code, look for two nested loops over the same list with a swap
inside - that is a bubble sort, even when it has a different name. Replace it
with the built-in sort and a key function.
Do this project yourself. It's ready in a live cloud workspace in your browser - when you finish, it lands on your portfolio.
Start this project - freeWhat you'll practice
- Recognising a hand-written bubble sort as O(n^2)
- Replacing it with sorted() using key and reverse=True
- Sorting by several fields with tuple keys and stable multi-pass sorts
- Timing the change and asserting the output order in a test
FAQ
How do I sort a list of objects by an attribute in Python?
Use sorted(items, key=lambda x: x.attribute) or items.sort(key=lambda x: x.attribute). operator.attrgetter("attribute") works as the key too.
How do I sort in descending order in Python?
Pass reverse=True to sorted() or list.sort(). For a number field inside a tuple key, you can also negate it, for example key=lambda u: (-u.score, u.name).
What is the difference between sorted() and list.sort()?
sorted() returns a new list and works on any iterable. list.sort() sorts the list in place and returns None. Both use the same algorithm and accept key and reverse.
Is Python's sort stable?
Yes. Items with equal keys keep their original order, also when you use reverse=True. This lets you sort by several fields in separate passes, secondary key first.
Why is bubble sort slow?
It compares and swaps neighbouring items over and over, which takes about n x n steps. Python's built-in sort takes about n log n steps, which is far less for large lists.
Keep learning
Do this project yourself. It's ready in a live cloud workspace in your browser - when you finish, it lands on your portfolio.
Start this project - free