Python Sorted Multiple Keys: Sort by Several Fields
python sorted multiple keys: Learn how to sort Python collections by multiple keys using sorted(), lambda, and itemgetter, with practical examples and performance notes.
When you need to sort a collection by more than one attribute, Python's sorted() function supports multiple keys through the key parameter. The python sorted multiple keys pattern is a common requirement when working with lists of dictionaries, tuples, or objects. The core idea is that the key function returns a tuple, and Python compares tuples element by element, so the first element has the highest priority, then the second, and so on.
How sorted() Handles Multiple Keys
sorted() takes a key callable that transforms each item into a sortable value. When you need multiple keys, the callable returns a tuple of values. Python's tuple comparison is lexicographic: it compares the first elements; if they differ, that determines the order; if they are equal, it compares the second elements, and so forth. This behavior is the foundation of multi-key sorting.
data = [ {'name': 'Alice', 'age': 30, 'score': 85}, {'name': 'Bob', 'age': 25, 'score': 92}, {'name': 'Alice', 'age': 25, 'score': 90}, ] sorted_by_name_age = sorted(data, key=lambda x: (x['name'], x['age']))
The lambda returns a tuple (name, age). The list is first sorted by name alphabetically; for entries with the same name, it sorts by age. This is the simplest way to apply multiple sort keys.
Using Lambda Functions for Multi-Key Sorting
Lambda functions are concise and work well for simple attribute access. You can combine any number of fields, and you can also apply transformations inside the lambda, such as lowercasing strings or extracting nested attributes.
records = [ {'last': 'Smith', 'first': 'John'}, {'last': 'Adams', 'first': 'Jane'}, {'last': 'Smith', 'first': 'Jane'}, ] sorted_records = sorted(records, key=lambda r: (r['last'].lower(), r['first'].lower()))
Here, both keys are normalized to lowercase to avoid case-sensitive ordering. The lambda returns a tuple of transformed values, which is perfectly valid. However, if your key logic becomes more complex—like accessing deeply nested fields or applying multiple transformations—a lambda can become hard to read. In those cases, a named function or operator.itemgetter is often clearer.
Using operator.itemgetter for Cleaner Code
The operator module provides itemgetter, which is a fast, readable way to extract multiple fields from a dictionary or sequence. itemgetter returns a callable that, given a sequence or mapping, returns a tuple of the requested keys or indices.
from operator import itemgetter users = [ {'name': 'Zoe', 'age': 30}, {'name': 'Amy', 'age': 25}, {'name': 'Zoe', 'age': 20}, ] sorted_users = sorted(users, key=itemgetter('name', 'age'))
itemgetter('name', 'age') is equivalent to lambda x: (x['name'], x['age']) but is implemented in C and often faster. It also works with lists and tuples using indices:
pairs = [(2, 'b'), (1, 'a'), (2, 'a')] sorted_pairs = sorted(pairs, key=itemgetter(0, 1))
This sorts by the first element, then by the second. itemgetter is the recommended choice when you only need to extract existing fields without transformation, because it is both concise and efficient.
Sorting by Multiple Keys with Mixed Directions
A common challenge is sorting some keys ascending and others descending. The reverse parameter of sorted() reverses the entire sort order, so you cannot directly specify per-key direction. There are two practical approaches.
Negate Numeric Values
For numeric fields, you can negate the value in the key function to invert the order for that specific key.
items = [ {'category': 'A', 'price': 10}, {'category': 'B', 'price': 5}, {'category': 'A', 'price': 15}, ] # Sort by category ascending, then price descending sorted_items = sorted(items, key=lambda x: (x['category'], -x['price']))
This works only for numbers. For strings, you cannot simply negate, but you can use a custom comparator via functools.cmp_to_key if you need mixed directions on non-numeric fields.
Sort Multiple Times Using Stability
Python's sort and sorted are stable, meaning that items that compare equal retain their original order. This allows you to sort in multiple passes, applying the least significant key first, then the more significant key. The final sort's key becomes the primary sort, and the previous sorts act as tie-breakers.
# Sort by price descending, then category ascending items.sort(key=lambda x: x['category']) # secondary key items.sort(key=lambda x: x['price'], reverse=True) # primary key
This technique works for any data type and avoids the need to negate values. It is especially useful when you need different directions for string keys. The tradeoff is that you perform multiple sort passes, but each pass is O(n log n) in the worst case, so the total cost is still acceptable for most datasets.
Stability and Its Role in Multi-Key Sorting
Stability is not just a side effect; it is a deliberate feature that enables the multi-pass approach above. When you call sorted() with a single key, equal elements keep their original relative order. This property is guaranteed by Python's sort algorithm (Timsort). Understanding stability helps you decide between a single tuple key and multiple passes. The tuple approach is usually more direct and avoids extra passes, but it cannot handle mixed directions without custom logic. The multi-pass approach is more flexible but requires you to order the sorts correctly: the final sort must be the primary key, and each earlier sort must be the next most significant key in reverse order.
Performance Considerations for Multi-Key Sorting
The main performance cost in multi-key sorting is the computation of the key function. sorted() calls the key function exactly once per item, so if the key function is expensive (e.g., it performs complex calculations or accesses external data), the overhead can dominate. To mitigate this, you can use the key parameter with a function that is as simple as possible. operator.itemgetter is implemented in C and is typically faster than a Python lambda for simple field access. If you need to apply transformations, consider precomputing the sort keys into a separate list and sorting that, or using the key parameter with a local function that avoids repeated attribute lookups.
Another consideration is memory. The key function returns a tuple for each item, and sorted() stores these tuples internally to perform the sort. For very large lists, this can increase memory usage. The multi-pass approach also creates intermediate sorted lists, but it does not store tuples of keys. In practice, the difference is small unless you are sorting millions of items. If memory is a concern, you can use list.sort() in place to avoid creating a new list, but the key tuples still exist during the sort.
Common Mistakes and Edge Cases
One frequent mistake is forgetting that tuple comparison is lexicographic and that all elements must be comparable. If your key tuple contains mixed types that cannot be compared, such as int and str, Python raises a TypeError. Ensure that all keys are of compatible types, or normalize them inside the key function.
Another issue is handling None values. If a field can be None, comparing it with other values may fail. You can provide a default value in the key function, such as lambda x: (x.get('name') or '', x.get('age') or 0). This converts None to a safe placeholder, but be aware that it changes the sort order for those entries.
Case sensitivity is also a common pitfall. If you sort strings without lowercasing, 'apple' comes before 'Banana' because uppercase letters have lower ASCII values than lowercase ones. If you want case-insensitive sorting, apply .lower() or .casefold() to the string keys.
Finally, remember that sorted() returns a new list, while list.sort() sorts in place. If you need to preserve the original list, use sorted(); if you want to save memory, use list.sort(). Both support the same key and reverse parameters, so the multi-key logic is identical.