pandas sort_values() — order rows by one or more columns

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.

What it does

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.

Why it matters

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.

Examples

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'.

Flags

FlagMeaning
bycolumn name or list of names to sort by; a list turns on lexicographic (multi-key) ordering
ascendingTrue/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
keyfunction applied to each sort column before ordering (pandas 1.1+) — natural-sort codes, lowercase text, sort by len()
ignore_indexTrue renumbers the result 0..n-1 instead of carrying the old labels along
inplaceexists but don't use it — chained .sort_values().head() reads better and inplace is not faster

Born in 0.11, matured by 1.0

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.

The key= design borrow

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.

One call, many sorters

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.

Fun facts

Pros

Cons

Takeaways