Sorting and reversing show up everywhere in an online store: products by price, customers by name, orders by date, recent activity at the top of a feed. Python ships two built-ins for this, sorted() and reversed(), and both work on more than just lists. This lesson covers what each one does, how to control the order with key and reverse, why Python's sort is stable and why that matters, and the small but real difference between reversed(seq) and slicing with [::-1].
The sorted() built-in takes any iterable and returns a brand new list with the items in order. The original is untouched.
The input was a tuple, the output is a list. That holds for any iterable: sets, dict keys, generators, file objects, ranges. If you can loop over it, you can sort it.
Sorting a dict sorts the keys, same as iterating a dict. To sort by value, pass a key function (more on that in a moment).
Strings sort character by character using Unicode code points. Uppercase letters come before lowercase ones because their code points are smaller.
That's almost never what you want for human-readable names. The fix is a key function, which the next section covers.
Cost: sorted() is O(n log n) for n items and uses O(n) extra memory because it builds a new list. For very large lists where you don't need the original order preserved, list.sort() sorts in place and avoids the allocation.
Pass reverse=True to flip the result from smallest-first to largest-first.
reverse=True is exactly the same result you'd get from sorted(prices)[::-1], but it does it in one pass instead of two and reads more cleanly. Use the keyword argument.
By default sorted() compares items directly. That works when the items are numbers or simple strings, but the moment you have richer data (dicts, tuples, objects), you need to tell sorted() which piece of each item to compare. That's what key does.
key is a function that takes one item and returns the value to sort on. Python calls this function on every item, then sorts by the returned values.
The lambda pulls p["price"] out of each dict, and sorted() orders the list by those prices. The dicts themselves are never compared directly.
Combine key and reverse to get "highest rated first":
The earlier case-insensitive name problem fixes the same way:
str.lower is a function (technically an unbound method). Python calls it on each name to get the value used for comparison. The original strings stay in their original case, only the comparison sees the lowercase version.
Lambdas are flexible, but for the common case of "pull this key out of a dict" or "pull this attribute off an object", the operator module has two helpers that are both faster and easier to read.
operator.itemgetter("price") returns a function that takes a dict (or any subscriptable object) and returns dict["price"]. It does the same thing as lambda p: p["price"], but it's implemented in C and runs noticeably faster in tight loops.
For objects (instances of a class), use attrgetter:
itemgetter and attrgetter both accept multiple names, which gives you multi-key sorting for free.
To sort by more than one field (rating first, then price as a tiebreaker), return a tuple from the key function. Python compares tuples element by element, so (4.5, 29.99) < (4.5, 49.99) because the first elements tie and the second one decides.
Two things in that tuple are worth pointing out. First, -p["rating"] flips the rating sort to descending without using reverse=True (which would flip both fields). Second, the price stays ascending because the second element is p["price"] (positive). That mixed-direction trick is one of the most common multi-key patterns.
itemgetter handles the same case when both directions are the same:
When you need mixed directions and the values aren't numeric (so you can't negate them), the trick is to sort twice and rely on stability, which is the next topic.
Python's sort algorithm is called Timsort, named after Tim Peters who designed it for CPython in 2002. It's a hybrid of merge sort and insertion sort, tuned for real-world data. One of its guarantees matters every day: it's stable.
A stable sort preserves the original order of items that compare equal. If two products have the same rating, the one that came first in the input list stays first in the output.
Wireless Mouse and HDMI Cable both have rating 4.5. Wireless Mouse came first in the input, so it comes first in the output. That ordering is guaranteed, not a coincidence.
Stability unlocks a powerful technique: sort multiple times, from least important key to most important. Each sort keeps the order of the previous one for equal items.
Same result as the tuple-key version, but each sort step uses its own reverse argument. That's useful when one key is a string and the other is a number, or anything else that can't be negated cleanly.
The diagram shows the two-pass sort. The first sort groups items by price. The second sort groups them by rating, and Timsort's stability guarantees that items with the same rating keep their price order from step one.
Cost: Each sort pass is O(n log n). Two passes is 2x the work of one pass, but it's still O(n log n) overall. For most lists this difference is irrelevant. The tuple-key approach is usually faster because it sorts once, but the multi-pass approach is more readable for complex mixed-direction sorts.
reversed() takes a sequence and gives you back an iterator that yields the items in reverse order. It does not return a list, and it does not modify the original.
Two things to watch. First, reversed() returns an iterator, so you can only walk it once. After the for loop above, the iterator is exhausted. Second, it works on sequences (anything with a length and integer indexing): lists, tuples, strings, ranges. It doesn't work on plain sets or generators, because those don't support indexed access.
If you need the reversed result as a list, wrap the call with list(). If you just want to loop over the items in reverse, the iterator is enough and avoids the allocation.
For your own classes, define __reversed__ and reversed() will pick it up:
This is rare in everyday code, but it's how collections.deque and similar containers make reversed() work efficiently.
You can also reverse a sequence with the slice seq[::-1]. It looks shorter, but it's not quite the same thing.
The slice runs immediately and allocates a new list of the same size as the input. reversed() builds an iterator object that holds a reference to the original sequence and walks it backwards on demand. For a million-item list, the slice copies a million references; reversed() allocates a tiny iterator object regardless of size.
Cost: reversed(seq) is O(1) and uses constant memory (it's lazy). seq[::-1] is O(n) time and O(n) extra memory because it builds a full reversed copy. For just iterating once in reverse, reversed() is always cheaper. For "give me a reversed list to pass somewhere else", use the slice (or list(reversed(seq)), which has the same cost).
| Operation | Returns | Time | Memory | Use when |
|---|---|---|---|---|
reversed(seq) | Iterator | O(1) to build, O(n) to consume | O(1) | You're looping once in reverse |
seq[::-1] | New list (or new string/tuple) | O(n) | O(n) | You need a reversed copy to index, pass, or reuse |
The slice has one quiet bonus: it works on more than sequences in a generic way for any object that supports slicing, including strings (where reversed() works fine too) and bytes. The big practical difference is the laziness.
Actually, the indices restart at 0 because enumerate doesn't know about the original positions. If you want the original index alongside the reversed item, you have to be careful:
reversed(range(len(items))) is O(1) and gives you the original indices in reverse. The slice version range(len(items))[::-1] would build a new range object too, but reversed(range(...)) is the more idiomatic Python here.
Lists have their own .sort() method. The two look almost identical, but they have one important difference, and a few smaller ones.
| Feature | sorted(iterable) | list.sort() |
|---|---|---|
| Returns | New list | None |
| Modifies original | No | Yes (in place) |
| Works on | Any iterable | Lists only |
| Memory | O(n) extra | O(1) extra (in place) |
Accepts key and reverse | Yes | Yes |
Use sorted() when:
sorted(...) returns a list you can pass to another function.Use list.sort() when:
The single most common bug with list.sort() is forgetting it returns None:
The fix is either to use sorted() if you want a returned value, or to sort in place and use the original variable:
10 quizzes