Two ways to get the largest first. They agree on everything except the records that tie — which is most of the interesting ones.
rows = [
{'name': 'ada', 'score': 5},
{'name': 'grace', 'score': 5},
{'name': 'linus', 'score': 1},
]
[r['name'] for r in sorted(rows, key=lambda r: r['score'], reverse=True)]
# -> ['ada', 'grace', 'linus'] ties keep their original order
[r['name'] for r in sorted(rows, key=lambda r: r['score'])[::-1]]
# -> ['grace', 'ada', 'linus'] ties are now backwards
- Python’s sort is stable, and reverse=True protects that
A stable sort leaves records that compare equal in their original relative order. reverse=True is implemented so that stability still holds — it reverses the comparison, not the result. Sorting ascending and then reversing the list flips the ties along with everything else, which is why the two lines above disagree.
- The key function decides, the record travels
key= extracts the value to compare while the sort moves the whole record, so what comes back is the original dictionaries, not the scores. That is what makes it possible to sort by one field and then read another. The key is called once per record — not once per comparison — so an expensive key function is affordable.
- Slicing after sorting is where the count belongs
sorted(...)[:count] handles 'more than there are' with no check, because a slice clamps. It does sort everything first, which is wasteful for three out of a million — that is what heapq.nlargest is for, and it is the one situation where reaching past sorted() pays.
Guard the count, check the records have the field, then sort by that field descending and take a slice. The order of those steps is the whole function.
sorted(records, key=..., reverse=True) — not sorted(...)[::-1], which flips records that tie. Use sorted rather than records.sort(), so the caller’s list keeps its own order.
operator.itemgetter(field) is the key function, and it is faster and clearer than a lambda for a plain field lookup. Check for the missing field with an explicit pass over the records first — relying on the KeyError from inside the sort gives the caller an exception with no context.
from operator import itemgetter def top_n_by(records, field, count): if count < 0: raise ____(f’count cannot be negative, got {count}') for record in records: if field not in record: raise ValueError(f’record is missing {field!r}: {record!r}') ranked = ____(records, key=itemgetter(field), ____=True) return ranked[:____]
from operator import itemgetter
def top_n_by(records, field, count):
if count < 0:
raise ValueError(f"count cannot be negative, got {count}")
for record in records:
if field not in record:
raise ValueError(f"record is missing {field!r}: {record!r}")
# reverse=True rather than [::-1]: a stable sort keeps tied records
# in their original order, and reversing the result would undo that.
ranked = sorted(records, key=itemgetter(field), reverse=True)
return ranked[:count]
Letting itemgetter raise KeyError inside sorted() does technically refuse the bad data — with an exception that names the key and not the record, thrown from somewhere in the sort, and of the wrong type for a caller catching ValueError. One explicit pass costs a single traversal and produces an error somebody can act on.
records.sort() would give the identical ranking and permanently reorder the caller’s list — so the next function to iterate it sees a different order for reasons that are three call frames away. A function that answers a question should not rearrange the thing it was asked about.
The returned list is new, but the dictionaries inside it are the caller’s own. Mutating one of them changes what the caller sees elsewhere, which is usually what you want — the alternative is copying every record on every call — and is worth knowing before you 'annotate' a top-ten result with a rank field.
For a small count out of a very large collection, heapq.nlargest(count, records, key=itemgetter(field)) is the right call: it keeps a heap of the best count seen so far instead of sorting everything, which is O(n log count) rather than O(n log n) and works on an iterator you cannot hold in memory. It is stable too. If the data is in a database, ORDER BY ... LIMIT belongs in the query, where an index can serve it without reading every row. Reach for this function when the list is already in hand and modest in size.
- Add a tie-break field, so equal scores are ordered by name, and decide whether that should replace the arrival-order rule or come after it.
- Swap in heapq.nlargest and confirm every case still passes, including the tie one.
- Support a dotted path for the field by reusing deep-get, and work out what 'missing' now means when a parent key is absent.