sort, argsort, searchsorted - and why the argument version is the one you usually need.
Overview
Copy or in place
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 convention for operations that have both forms, and it is worth relying on: if you see a bare method call with no assignment, it is modifying something.
There is no reverse= argument. Sort ascending and reverse with [::-1], which 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 argument version is usually 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
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
Output
argsort gives the order, not the values
This is the one you want whenever other arrays must follow the sort.
example_03.pyNumPy
Output
Sorting by several keys with lexsort
The last key is the primary one, which reads backwards and catches everyone.
example_04.pyNumPy
Output
Partial sorting when you only need the top k
argpartition is O(n) and does not bother ordering the rest.
example_05.pyNumPy
Output
searchsorted finds insertion points in a sorted array
Binary search, so it is fast, and it is how you bucket values.
example_06.pyNumPy
Output
The axis default
On a 2-D array, sort() sorts along the last axis — each row independently.
This is a genuinely dangerous default when the array is a table. Sorting a (rows, columns) array of records scrambles every row internally: the name column gets sorted, the salary column gets sorted, and the correspondence between them is destroyed.
sort(axis=None) flattens and does one global sort. sort(axis=0) sorts each column independently, which 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 positions 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 is also how you produce 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, which is not stable. Equal elements can come out in any order. Pass kind="stable" when ties must preserve their original relative order — for instance when you are sorting by a second key after already sorting by a first.
Several keys
np.lexsort sorts by multiple keys, and the argument order reads backwards: the last array is the primary key.
np.lexsort((salary, dept)) sorts by department, breaking ties by salary.
Everyone 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, and it does not order anything else — it only guarantees the partition 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 positive index, and for medians, which is what np.median uses internally.
searchsorted
Given a sorted array, np.searchsorted(edges, values) returns where each value would be inserted to keep it sorted. It is a binary search, so it is fast even on large arrays.
Two uses come up constantly.
Bucketing. Given cut points, searchsorted gives the bucket index for each value directly, and indexing a label array with the result assigns categories 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 comparisons.
For pure membership testing where order does not matter, np.isin is simpler and does not require sorted input.
The mistakes
Sorting a table with sort() and destroying the row correspondence. Use argsort on a key column.
Assuming stability. The default is not stable; pass kind="stable" when it matters.
Getting lexsort backwards. The last key is primary.
Full sorting for a top-n.argpartition exists precisely for that.
The sort kinds
kind selects the algorithm, and the choice occasionally matters.
"quicksort" is the default, an introsort in practice. Fastest on average, not stable, and its worst case is handled by falling back to heapsort.
"stable" guarantees that equal elements keep their original relative order. It is what you need when sorting by a second key after already sorting by a first, since an unstable sort would discard the first ordering.
"heapsort" has a guaranteed worst case and uses no extra memory, which is occasionally the deciding factor.
"mergesort" is an alias for "stable".
The performance difference is modest for most data. Stability is not a performance question, and 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, and it works because the structured dtype makes each row a single comparable item.
For plain 2-D numeric arrays, there is no order argument, and 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 produces the index and the same fancy-index application 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 argument 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 coordinates.
np.flatnonzero(mask) is the flat-index version, and is usually 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 condition", np.argmax(mask) is the fast answer, because it stops at the first True. It returns 0 when nothing matches, which is indistinguishable from a match at position zero, so check mask.any() first. There is no built-in "find first" that handles the empty case, and this two-step is the idiom.
searchsorted in more detail
searchsorted requires its first argument to be sorted, and does not check. Passing unsorted data gives wrong answers silently, which is the main way it goes wrong.
side="left" returns the first position where the value could be inserted; side="right" returns the last. They differ only for values that are already present, and the choice decides which bucket a boundary value falls into.
For bucketing, side="right" with cut points means a value exactly on a boundary goes into the upper bucket, which matches the usual convention 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 — useful when the array must keep its original order for other reasons.
np.digitize is a related function with a right argument 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 positions in sorted order. np.argsort(np.argsort(x)) gives, for each element, its rank.
That double application is unintuitive the first time and worth the moment it takes to see: the first argsort answers "which element belongs at each rank", and inverting that mapping answers "which rank does each element get".
Ties get consecutive distinct ranks rather than being averaged, which is not what most statistical definitions of rank want. scipy.stats.rankdata handles tie-breaking properly, and 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 comparisons.
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 is roughly an order of magnitude.
searchsorted is O(log n) per lookup against an already-sorted array, so m lookups cost O(m log n). If you are searching the same array repeatedly, sorting it once and using searchsorted beats any linear scan.
np.isin builds a sorted structure internally, so it is O((n+m) log(n+m)) — better than the naive comparison, and worth knowing that it is not free either.
Sorting along an axis of a higher-dimensional array
sort takes an axis like every other operation, and the default is -1 — the last one.
On a 3-D array that means each 1-D line along the last axis is sorted independently. That is rarely wrong but frequently not what was intended, and passing axis explicitly makes the intent visible.
argsort on a multi-dimensional array returns indices along that axis only, not flat positions. Applying them requires np.take_along_axis:
That is the multi-dimensional version of the sort-by-key idiom, and it is the function people usually fail to find. np.put_along_axis is the assignment counterpart.
Descending, and the sign trick
There is no reverse= argument 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, because it keeps the index arithmetic straightforward: np.argsort(-scores) gives descending order directly.
That works for numeric data. It does not work for unsigned integers, where negation wraps, and it is a subtle way to get a wrong answer on uint8 image data.
Searching by value
There is no index() method. Finding where a value occurs is a comparison followed by a search:
np.flatnonzero(a == value) gives every position, 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 position in logarithmic time, which is the right choice when searching repeatedly.
For membership across two arrays, np.isin handles it in one call rather than a loop of comparisons.
The summary
np.sort copies; a.sort() modifies. That distinction holds throughout NumPy.
argsort is the one to use whenever anything else must follow the ordering, which in practice is most of the time.
kind="stable" when ties must keep their original order, which includes any multi-pass sort by successive keys.
lexsort for several keys, remembering that the last one is primary.
argpartition for top-k, which is linear rather than n log n.
searchsorted for insertion points and bucketing, on data that is genuinely sorted — it does not check.
take_along_axis when applying argsort results on more than one dimension.
Between them those cover essentially every ordering question, and the two that get used most are argsort and argpartition.
One more thing about stability
Stability is not a performance setting, and it is 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 produce different output, and the difference only appears when there are ties. Code that sorts by a secondary key and then by a primary key relies entirely on the second sort preserving the first ordering, and with the default kind it does not.
Whenever a sort follows another sort, or whenever the output order of tied records is observable, pass kind="stable". The cost is small and the alternative is a bug that reproduces intermittently.
Where sorting fits
Sorting is rarely the goal in itself. It is the step that makes something 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 is not needed, argsort or argpartition usually 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
That is NumPy's general convention for operations that have both forms, and it is worth relying on: if you see a bare method call with no assignment, it is modifying something.
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.