sort, argsort, searchsorted - and why the argument version is the one you usually need.
Overview
Copy or in place
NumPy provides two ways to sort an array. np.sort returns a new sorted array. The method a.sort modifies the array in place and returns None. That distinction follows NumPy's general convention: the function form is non-destructive, the method form mutates.
Knowing which one you used matters because in-place sorting on a view changes the underlying array. If you have a slice of a larger array and sort it in place, the original array sees those rows rearranged. That is sometimes the intent and sometimes a bug. The function form avoids the question by always returning a copy.
For multi-dimensional arrays, the axis parameter decides which axis to sort along. The default is the last axis, which sorts each row independently in a 2-D array. axis=0 sorts each column independently. Neither rearranges whole rows or whole columns. If you want to sort rows by the values in one column, you need argsort.
argsort returns the indices that would sort the array. You can then use those indices to reorder both the array and any related arrays in the same way. That pattern is how you sort a matrix by one column while keeping the rows intact.
searchsorted finds insertion points in a sorted array. Given a sorted array and a set of values, it returns the index where each value would go to keep the array sorted. That is useful for binning, for assigning labels to ranges, and for any task where you have boundaries and need to know which bin a value falls into.
partition is a faster alternative when you need only the k smallest or largest values and do not care about their order. It rearranges the array so that the element at position k is where it would be in a fully sorted array, with smaller elements before it and larger elements after. That is an O(n) operation rather than O(n log n).
This module covers sort, argsort, searchsorted, partition, and argpartition. The examples show sorting in one and two dimensions and the relationship between argsort and fancy indexing.
np.sort(a) returns a new sorted array and leaves a alone. a.sort() sorts a itself and returns None.
That is NumPy's general rule for steps that have both forms. It's worth leaning on. If you see a bare method call with no set, it's modifying a thing.
There's no reverse= arg. Sort ascending and reverse with [::-1]. This Is a free view.
Worth knowing
np.sort(a) returns a sorted copy; a.sort() sorts in place. That function-versus-method convention holds across NumPy.
There is no reverse=. Sort ascending and slice with [::-1].
On a 2-D array the default sorts each row independently. Rows do not stay together.
argsort returns positions, so you can apply the same order to several arrays and keep them aligned.
lexsort takes the last key as primary — the reverse of how it reads.
argpartition gets the top k in linear time without ordering the rest; searchsorted does binary search for insertion points and bucketing.
Sorting and Searching
sort, argsort and searchsorted, and why the arg version is often the one you need.
sort copies, .sort() is in place
NumPy's consistent convention: the function returns a new array, the method modifies.
example_01.pyNumPy
import numpy as np
a = np.array([3, 1, 4, 1, 5])
b = np.sort(a)
print("np.sort(a):", b, " a unchanged:", a)
a.sort()
print("a.sort() :", a, " <- a itself is now sorted")
print()
print("There is no reverse= argument. Sort, then flip:")
c = np.array([3, 1, 4, 1, 5])
print(" descending:", np.sort(c)[::-1])
Output
Sorting a 2-D array sorts each row separately
By default it sorts along the last axis, which is almost never a whole-array sort.
example_02.pyNumPy
import numpy as np
a = np.array([[7, 1, 9],
[3, 8, 2]])
print("original:"); print(a)
print()
print("sort() -> along axis=-1, each ROW independently:")
print(np.sort(a))
print()
print("sort(axis=0) -> each COLUMN independently:")
print(np.sort(a, axis=0))
print()
print("sort(axis=None) -> flatten first, one global sort:")
print(np.sort(a, axis=None))
print()
print("Note rows are sorted in isolation - they do not stay together.")
Output
argsort gives the order, not the values
This is the one you want whenever other arrays must follow the sort.
example_03.pyNumPy
import numpy as np
names = np.array(["ana", "raj", "kim", "lee"])
score = np.array([72, 91, 58, 84])
order = np.argsort(score)
print("scores :", score)
print("argsort :", order, "<- positions, in ascending order of score")
print()
print("apply it to BOTH arrays and they stay aligned:")
print(" names :", names[order])
print(" score :", score[order])
print()
print("descending is the same order reversed:")
top = np.argsort(score)[::-1]
print(" ranking:", list(zip(names[top].tolist(), score[top].tolist())))
Output
Sorting by several keys with lexsort
The last key is the primary one, which reads backwards and catches everyone.
example_04.pyNumPy
import numpy as np
dept = np.array(["b", "a", "b", "a"])
sal = np.array([50, 70, 40, 90])
order = np.lexsort((sal, dept)) # dept first, then sal within it
print("lexsort((sal, dept)) -> LAST argument is the primary key")
for i in order:
print(" %s %d" % (dept[i], sal[i]))
print()
print("swap them and salary becomes primary:")
for i in np.lexsort((dept, sal)):
print(" %s %d" % (dept[i], sal[i]))
Output
Partial sorting when you only need the top k
argpartition is O(n) and does not bother ordering the rest.
example_05.pyNumPy
import numpy as np
import time
rng = np.random.default_rng(0)
x = rng.random(200_000)
t = time.perf_counter(); full = np.argsort(x)[-5:]; t1 = time.perf_counter() - t
t = time.perf_counter(); part = np.argpartition(x, -5)[-5:]; t2 = time.perf_counter() - t
print("full argsort : %.4f s" % t1)
print("argpartition : %.4f s" % t2)
print("same 5 values :", np.array_equal(np.sort(x[full]), np.sort(x[part])))
print()
print("argpartition guarantees the top 5 are in the last 5 slots,")
print("but NOT that they are in order. Sort just those five if you care.")
print(" ordered top 5:", np.round(np.sort(x[part])[::-1], 5))
Output
searchsorted finds insertion points in a sorted array
Binary search, so it is fast, and it is how you bucket values.
On a 2-D array, sort() sorts along the last axis. Each row on its own.
This is a truly dangerous default when the array is a table. Sorting a (rows, columns) array of records scrambles each row inside. The name column gets sorted, the salary column gets sorted. The correspondence between them is destroyed.
sort(axis=None) flattens and does one global sort. sort(axis=0) sorts each column on its own. This Has the same problem in the other direction.
None of these sort rows as units. For that you need argsort on a key column and then fancy indexing.
argsort is the useful one
np.argsort(x) returns the spots that would sort x.
That indirection is the point. Apply the same index array to any number of parallel arrays and they stay aligned:
order = np.argsort(score)
names[order], score[order], dates[order]
This is how you sort a table by a column. It's also how you give a ranking, take the top n, or reorder anything that travels alongside the sorted values.
Descending is np.argsort(x)[::-1].
One subtlety: the default sort is quicksort. This Isn't stable. Equal elements can come out in any order. Pass kind="stable" when ties must preserve their first scaled order. say when you're sorting by a second key after already sorting by a first.
Several keys
np.lexsort sorts by multiple keys, and the arg order reads backwards: the last array is the main key.
np.lexsort((salary, dept)) sorts by department, breaking ties by salary.
all gets this wrong the first time. The mnemonic that helps. The keys are applied in order from last to first, like a stable sort chain where the final pass dominates.
Only the top k
Sorting 200,000 values to look at 5 of them is wasteful.
np.argpartition(x, -5) rearranges so that the 5 largest occupy the last 5 slots, in linear time. It does not order them. It doesn't order anything else. It only promises the split boundary.
If you need those five in order, sort just those five. That is five elements instead of two hundred thousand.
The same trick works for the smallest k with a plus index. For medians. This Is what np.median uses inside.
searchsorted
Given a sorted array, np.searchsorted(edges, values) returns where each value would be inserted to keep it sorted. It's a binary search. It's fast even on large arrays.
Two uses come up constantly.
Bucketing. Given cut points, searchsorted gives the bucket index for each value right. Indexing a label array with the result assigns groups in one line. side="right" versus side="left" decides which bucket an exact boundary value falls into. The only case where they differ.
Lookup. Finding many values in a large sorted array is O(m log n) this way, against O(mn) for repeated checks.
For pure membership testing where order doesn't matter, np.isin is simpler and doesn't need sorted input.
The mistakes
**Sorting a table with sort()** and destroying the row correspondence. Use argsort on a key column.
given steadiness. The default isn't stable. Pass kind="stable" when it matters.
**Getting lexsort backwards.** The last key is main.
Full sorting for a top-n.argpartition exists just for that.
The sort kinds
kind selects the method, and the choice at times matters.
"quicksort" is the default, an introsort . Fastest on average, not stable. Its worst case is handled by falling back to heapsort.
"stable" promises that equal elements keep their first scaled order. It's what you need when sorting by a second key after already sorting by a first. An unstable sort would discard the first sorting.
"heapsort" has a sure worst case and uses no extra memory. This Is at times the deciding factor.
"mergesort" is an alias for "stable".
The speed gap is modest for most data. Steadiness isn't a speed question. When you need it, you need it.
Sorting records
Structured arrays — arrays with named fields — can be sorted by field name:
a.sort(order=["dept", "salary"])
This is the closest NumPy comes to sorting a table by columns. It works because the structured dtype makes each row a single close item.
For plain 2-D numeric arrays, there's no order arg. The answer is the argsort idiom. Sort the key column, apply the resulting index to the whole array with fancy indexing. a[np.argsort(a[:, 2])] sorts rows by the third column.
For more than one key, lexsort makes the index and the same fancy-index use follows.
where, nonzero, and finding things
np.nonzero(mask) returns the indices of True elements, as a tuple with one array per axis. np.where(mask) with a single arg is the same function.
The tuple form is designed to be used as an index: a[np.nonzero(mask)] selects the matching elements. Unpacking it with rows, cols = np.nonzero(mask) gives coords.
np.flatnonzero(mask) is the flat-index version, and is often what you want for a 1-D array. It returns a plain array rather than a one-element tuple.
For "the first element matching a rule", np.argmax(mask) is the fast answer. It stops at the first True. It returns 0 when nothing matches. This Is the same from a match at spot zero. Check mask.any() first. There's no built-in "find first" that handles the empty case. This two-step is the idiom.
searchsorted in more detail
searchsorted needs its first arg to be sorted. Doesn't check. Passing unsorted data gives wrong answers silently. This Is the main way it goes wrong.
side="left" returns the first spot where the value could be inserted. side="right" returns the last. They differ only for values that are already present. The choice decides which bucket a boundary value falls into.
For bucketing, side="right" with cut points means a value just on a boundary goes into the upper bucket. This Matches the usual rule for grade boundaries and histogram bins.
sorter lets you pass an argsort result so the lookup can be done against an unsorted array without sorting it first. handy when the array must keep its first order for other reasons.
np.digitize is a related function with a right arg whose meaning is the reverse of what the name suggests in one of its branches. searchsorted is easier to reason about, and does the same job.
Ranking
A ranking is argsort applied twice.
np.argsort(x) gives the spots in sorted order. np.argsort(np.argsort(x)) gives, for each element, its rank.
That double use is unintuitive the first time and worth the moment it takes to see. The first argsort answers "which element belongs at each rank". Inverting that mapping answers "which rank does each element get".
Ties get in a row distinct ranks rather than being averaged. This Isn't what most statistical meanings of rank want. scipy.stats.rankdata handles tie-breaking properly. Is the right tool when the ranking is going into a statistic.
The performance summary
Sorting is O(n log n), and argsort costs slightly more than sort because it moves indices alongside checks.
argpartition is O(n) and is the right choice for any top-k or bottom-k question. The saving is large. For the top 5 of 200,000 values it's roughly an order of magnitude.
searchsorted is O(log n) per lookup against an already-sorted array. m lookups cost O(m log n). If you're searching the same array again and again, sorting it once and using searchsorted beats any linear scan.
np.isin builds a sorted structure inside. It's O((n+m) log(n+m)). Better than the naive compare. Worth knowing that it isn't free either.
Sorting along an axis of a higher-dimensional array
sort takes an axis like every other step. The default is -1 — the last one.
On a 3-D array that means each 1-D line along the last axis is sorted on its own. That is rarely wrong but often not what was intended. Passing axis by name makes the intent visible.
argsort on a multi-dimensional array returns indices along that axis only, not flat spots. Applying them needs np.take_along_axis:
That is the multi-dimensional version of the sort-by-key idiom. It's the function people often fail to find. np.put_along_axis is the set counterpart.
Descending, and the sign trick
There's no reverse= arg anywhere in NumPy's sorting.
np.sort(a)[::-1] reverses the result, and the slice is a free view.
For argpartition and argsort, negating the values is often cleaner than reversing the result. It keeps the index math straightforward: np.argsort(-scores) gives descending order right.
That works for numeric data. It doesn't work for unsigned ints. Here Negation wraps. It's a subtle way to get a wrong answer on uint8 image data.
Searching by value
There's no index() method. Finding where a value occurs is a compare followed by a search:
np.flatnonzero(a == value) gives each spot, as a plain array.
np.argmax(a == value) gives the first, in one pass, with the caveat that it returns 0 when nothing matches.
For a sorted array, np.searchsorted finds the spot in logarithmic time. This Is the right choice when searching again and again.
For membership across two arrays, np.isin handles it in one call rather than a loop of checks.
The summary
np.sort copies. a.sort() modifies. That gap holds throughout NumPy.
argsort is the one to use when anything else must follow the sorting. This is most of the time.
kind="stable" when ties must keep their first order. This Includes any multi-pass sort by successive keys.
lexsort for a few keys, remembering that the last one is main.
argpartition for top-k, which is linear rather than n log n.
searchsorted for insertion points and bucketing, on data that is truly sorted — it doesn't check.
take_along_axis when applying argsort results on more than one dim.
Between them those cover each sorting question. The two that get used most are argsort and argpartition.
One more thing about stability
Steadiness isn't a speed setting, and it's worth one more paragraph because the cost of getting it wrong is invisible.
An unstable sort is free to reorder equal elements arbitrarily. That means two runs on the same data can give other output. The gap only appears when there are ties. Code that sorts by a secondary key and then by a main key leans fully on the second sort preserving the first sorting. With the default kind it doesn't.
When a sort follows another sort, or when the output order of tied records is observable, pass kind="stable". The cost is small and the option is a bug that copies intermittently.
Questions people ask about Sorting and Searching
What is the difference between sort and argsort? sort returns the sorted values, while argsort returns the indices that would sort the array. Use argsort to reorder a second array alongside the first.
How do I sort in descending order? Sort ascending and reverse with a[::-1], or negate numeric values. There is no reverse flag on np.sort.
Does np.sort sort in place? np.sort returns a copy. The method a.sort() sorts in place and returns None.
How do I find where to insert a value in a sorted array? Use np.searchsorted. It does a binary search, so it is far faster than scanning for large arrays.
How do I get the k largest values quickly? Use np.partition or argpartition. They place the k-th element correctly without sorting everything, which is faster for large arrays.
Is the sort stable? Use kind='stable' to guarantee that equal elements keep their original order. The default quicksort does not promise that.
Where sorting fits
Sorting is rarely the goal in itself. It's the step that makes a thing else possible. A ranking, a top-n, a binary search, a group-by on the boundaries where a key changes, a merge of two datasets.
That framing helps when choosing. If the sorted array itself isn't needed, argsort or argpartition often is. And if you only need the order to apply it elsewhere, sorting the values was wasted work.
Check yourself
0 of 4
Answer without scrolling back up.
What does `a.sort()` do on a 2-D array by default?
On a table this destroys the correspondence between columns. To sort rows as units, argsort a key column and index with the result.
Why use `argsort` rather than `sort`?
That indirection is the whole point - it is how you sort a table by a column and keep every other column aligned.
In `np.lexsort((salary, dept))`, which is the primary key?
The argument order reads backwards, and everyone gets it wrong the first time. The keys are applied last to first.
You need the 5 largest of 200,000 values. What is the right tool?
Linear time instead of n log n. It does not order the top 5 - sort just those five afterwards if you need them ranked.
Cheat sheet
Sorting and Searching
Sort a NumPy array, get sorted indices with argsort and find insertion points with searchsorted. See why argsort is usually the more useful call.
Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.