sorted() for DataFrames — but with stable ties, NaN control, and multi-key ordering built in.
Your 'top 10 customers' report is a lie if the sort underneath it is unstable — here's the flag that fixes it.
sort_values() returns your DataFrame (or Series) reordered by the values in one or more columns. Pass one column name and ascending=False for a top-N, or a list of columns with a matching ascending list for lexicographic ordering — sort by region first, then by revenue within each region. NaNs drift to the end by default; na_position moves them. Since pandas 1.1, key= accepts a function applied to the sort column before ordering, so 'M-20' can sort after 'M-3' instead of as text.
Every human-facing deliverable starts with an order: leaderboards, ranked incident lists, 'top 5 slowest queries'. Sorting is also the quiet prerequisite for things people reach for groupby for — head(3) after a sort gives per-group leaders without touching groupby at all. And unlike Python's sorted(), the default quicksort does not preserve tie order, so two runs over the same data can shuffle equal rows differently. kind='stable' makes the result deterministic, which matters the moment a report is attached to a name.
import pandas as pd
cities = pd.DataFrame({
"city": ["Vienna", "Linz", "Graz", "Salzburg", "Innsbruck", "Klagenfurt"],
"country": ["Austria"] * 6,
"population_k": [1931, 331, 331, 155, 132, 101],
"area_km2": [414.6, 96.4, 127.6, 65.7, 113.0, 120.4],
})
top3 = cities.sort_values("population_k", ascending=False).head(3)
print(top3.to_string(index=False))city country population_k area_km2 Vienna Austria 1931 414.6 Linz Austria 331 96.4 Graz Austria 331 127.6
The classic leaderboard: sort descending, head(N). Linz ranks above Graz because 96.4 < 127.6 — the default quicksort kept that tie order, but don't rely on it (see the stable tip below).
# Two-key sort: region A→Z, then revenue high→low within each region
sales = pd.DataFrame({
"region": ["east", "west", "east", "west", "east", "west"],
"quarter": ["Q1", "Q1", "Q2", "Q2", "Q1", "Q2"],
"revenue": [120, 340, 95, 410, 150, 380],
})
out = sales.sort_values(["region", "revenue"], ascending=[True, False])
print(out.to_string(index=False))region quarter revenue east Q1 150 east Q1 120 east Q2 95 west Q2 410 west Q2 380 west Q1 340
ascending accepts a list matching the by list — mixed directions in one call. This is the pattern behind 'biggest deal per region' exports.
messy = pd.DataFrame({"score": [50, None, 90, 20, None]})
print(messy["score"].sort_values(na_position="first").to_string())
print()
# sort a code column numerically, not as text
df = pd.DataFrame({"part": ["M-4", "M-20", "M-3"]})
print(df.sort_values("part", key=lambda c: c.str.split("-").str[1].astype(int)).to_string(index=False))1 NaN 4 NaN 3 20.0 0 50.0 2 90.0 part M-3 M-4 M-20
na_position='first' surfaces incomplete rows instead of hiding them at the bottom. key= fixes the classic text-sort bug where 'M-20' sorts before 'M-3'.
| Flag | Meaning |
|---|---|
by | column name or list of names to sort by; a list turns on lexicographic (multi-key) ordering |
ascending | True/False, or a list of them — one direction per key, e.g. [True, False] for region A→Z then revenue high→low |
na_position | 'last' (default) or 'first' — where NaNs land; 'first' keeps broken rows visible in reports |
kind | 'quicksort' (default), 'stable', 'mergesort', 'heapsort' — only 'stable' guarantees equal keys keep their original order |
key | function applied to each sort column before ordering (pandas 1.1+) — natural-sort codes, lowercase text, sort by len() |
ignore_index | True renumbers the result 0..n-1 instead of carrying the old labels along |
inplace | exists but don't use it — chained .sort_values().head() reads better and inplace is not faster |
sort_values() arrived in pandas 0.11.0 (May 2013) as the renamed, consistent replacement for the confusing sort(). The old name clashed with numpy's axis semantics; the deprecation finished in 0.17 (October 2015) left one verb that always sorts by value, on either axis.
key= landed in pandas 1.1.0 (July 2020) and works like Python's sorted(key=...), applied per sort column before comparison. It closed a decade of Stack Overflow threads about sorting '10' after '9' in string columns — the same gap that made natural sort a rite of passage.
For a single column, sort_values() hands the NumPy array to the requested kind (quicksort by default). Multiple columns go column-by-column, right to left: the last key sorts first, each earlier key then stable-sorts over it — a radix sort in disguise. That's also why multi-key sorts come out grouped under the leftmost key: that final pass uses a stable algorithm by construction. Sorting on a column you already have as the index is cheaper with sort_index(), which skips the by-column lookup entirely.