Every language ships a general-purpose comparison sort, but the ergonomics of customizing that sort — by key, by multiple fields, by reversed order — and the guarantees you can rely on (especially stability) differ sharply enough to cause real interview bugs, particularly for anyone assuming JavaScript's sort() behaves like Python's or Java's by default. Go's slices.Sort/SortFunc is clean but has no key= lambda — you write a Less or a 3-way cmp.
Language Verdict: Pros, Cons & Recommendation
key= is called once per element (O(n)) via decorate-sort-undecorate, not once per comparison- Tuple keys (
(x.a, x.b)) give free lexicographic multi-field sorting with zero extra syntax sorted()/.sort() are unconditionally stable, and reverse=True preserves that stability instead of flipping tie orderoperator.attrgetter/itemgetter shave off the per-call lambda overhead for simple key extraction
- Mixing ascending and descending across multiple non-numeric fields needs
functools.cmp_to_key, since negating a key only works for numeric fields sorted() returns a new list while .sort() returns None — writing x = arr.sort() is a real, easy mistake
Comparator.comparing(...).thenComparing(...).reversed() chains readably and lets every field's direction be set independently, even non-numeric onesComparable/compareTo gives a class a real natural-ordering protocol, picked up automatically by Collections.sort and TreeSetArrays.sort(Object[])/Collections.sort are guaranteed stable (TimSort-derived)- No default-sort footgun — natural ordering on
int[]/Integer[] is always numeric
Arrays.sort(int[]) on primitives uses an unstable dual-pivot Quicksort — a real trap if you're sorting a parallel array by the same indicescompareTo implementations risk integer-overflow bugs from manual subtraction; must use Integer.compare(a, b) instead
slices.Sort on cmp.Ordered types is a one-liner with numeric (not lexicographic) orderingslices.SortFunc + cmp.Compare is overflow-safe 3-way compare; cmp.Or (1.22+) cascades tiebreakersslices.SortStable/SortStableFunc make the stable/unstable choice explicit by namesort.Slice with a Less is still widely recognized if the interviewer is on an older Go
- No
key= extractor — every custom sort writes a Less or 3-way cmp, and the key is recomputed per comparison - No
Comparable protocol for structs; slices.Sort will not pick up a method you define slices.Sort is not stable — you must remember SortStable when tie order matters
Array.prototype.sort has been spec-guaranteed stable since ES2019 in every modern engine- The comparator callback is a first-class, unambiguous negative/zero/positive style, no adapter function needed
- ES2023's
toSorted() gives a genuine non-mutating sort without the [...arr] copy idiom
- No key-extractor sugar at all — every sort recomputes both sides' keys inline, up to O(n log n) extractions vs. Python's O(n)
- Bare
.sort() on numbers sorts lexicographically by default (10 before 2) — one of the most common real interview bugs - No
Comparable-equivalent protocol — there's no way to give a class a natural ordering picked up automatically by anything .sort() mutates in place AND returns the same reference, easy to misread as a new array
Recommendation: Python's key= is still the typical DSA default for speed-of-writing. Java's Comparator.comparing().thenComparing() is the gold standard for multi-field sorts. Go is a solid nice-to-have (slices.Sort / SortFunc + cmp.Compare) if you remember there is no key= and that Sort is unstable. In JavaScript, never call bare .sort() on numbers — always pass (a, b) => a - b.
Coding Mechanics, Side by Side
Python and Java let you extract a sort key and let the language derive comparisons from it; Go and JavaScript require you to write the full comparator (Less / 3-way cmp / (a, b) => ...) yourself every time.
from operator import attrgetter
items.sort(key=lambda x: x.value) # in-place
result = sorted(items, key=attrgetter('value')) # new list
# the key function is called once per element (O(n)), not per comparison
key= is the idiomatic Python style — you supply a function from element to sort key, and Python calls it exactly once per element (decorate-sort-undecorate internally), then compares keys with their natural <. attrgetter/itemgetter from operator are marginally faster than an equivalent lambda since they avoid a Python-level function call per invocation.
This is one of the most common real interview bugs for anyone using JavaScript: calling .sort() with no comparator on an array of numbers does NOT sort numerically.
nums = [10, 1, 2]
print(sorted(nums)) # [1, 2, 10] — correct, natural numeric ordering by default
Python's default sorted()/.sort() uses each element's natural < ordering — for numbers, that's always numeric comparison. There is no equivalent trap: sorting a list of ints or floats with no key= just works correctly.
# tuple comparison is lexicographic: compares x.a first, x.b only on ties
items.sort(key=lambda x: (x.a, x.b))
# mixed ascending/descending: negate the numeric field you want descending
items.sort(key=lambda x: (x.a, -x.b))
Tuples compare element-by-element left to right, stopping at the first difference — exactly the semantics you want for 'sort by A, break ties by B'. Mixing ascending/descending fields is easy for numeric fields (negate the ones you want reversed) but requires functools.cmp_to_key for a non-numeric field you want reversed (e.g. a descending string field).
sorted(items, reverse=True) # simplest: literal reverse
sorted(items, key=lambda x: x.value, reverse=True) # reverse combined with a key
sorted(items, key=lambda x: -x.value) # negate instead (numeric only)
reverse=True reverses the FINAL ordering while staying stable — ties keep their original relative order — which is subtly different from negating the key (which flips comparisons but only works for numeric keys). For non-numeric keys where you want a reversed tiebreaker, reverse=True on the whole sort or a cmp_to_key-based comparator are the real options.
Stability — whether equal elements keep their original relative order — matters whenever you sort by one field but care about a secondary, implicit order (e.g. original insertion order, or a previous sort pass) for elements that tie.
# Timsort — ALWAYS stable, guaranteed by the language, for both sorted() and .sort()
data = [(1, 'b'), (1, 'a'), (0, 'z')]
data.sort(key=lambda x: x[0])
print(data) # [(0, 'z'), (1, 'b'), (1, 'a')] — the two 1's keep their relative order
Python guarantees stability for sorted()/list.sort() unconditionally — this is documented language behavior, not an implementation detail, and it's part of why 'sort by B first, then stable-sort by A' (two chained single-key sorts) reliably produces a correct multi-key sort in Python.
The classic '(a, b) -> negative/zero/positive' comparator style is native to Java, Go (slices.SortFunc), and JS but is the odd one out for Python, which defaults to key-extraction instead.
from functools import cmp_to_key
def compare(a, b):
if a.value != b.value:
return a.value - b.value
return 0
items.sort(key=cmp_to_key(compare))
Python's sorted/.sort only accept key=, never a raw two-argument comparator — functools.cmp_to_key exists specifically to adapt an old-style comparator function into something the key-based API can consume. You'll mostly reach for this when porting a comparator from another language, or when a comparison genuinely can't be expressed as 'extract a key and compare keys' (rare, but it happens with some custom transitive-ordering logic).
Every language distinguishes 'sort this collection in place' from 'give me a new sorted collection, leave the original alone' — mixing them up is a real, easy-to-make bug.
arr = [3, 1, 2]
new_list = sorted(arr) # returns a NEW list, arr untouched
arr.sort() # sorts arr IN PLACE, returns None
bug = arr.sort() # bug: bug is None, not the sorted list!
sorted() always returns a new list; .sort() always mutates in place and returns None. The classic mistake is x = arr.sort() expecting x to be the sorted list — it's actually None, since .sort()'s return value is intentionally None to signal 'this was a mutation, not a new value' (the same design philosophy as most in-place Python list methods).
Sorting a collection of custom objects with no explicit comparator requires each language to answer 'what does less than even mean for this type?' — Python, Java, Go, and JavaScript answer that question very differently.
from functools import total_ordering
@total_ordering
class Item:
def __init__(self, value):
self.value = value
def __eq__(self, other):
return self.value == other.value
def __lt__(self, other):
return self.value < other.value
items.sort() # uses __lt__ directly — no key= needed
Defining __lt__ (and __eq__) makes a class sortable with plain sorted(items)/items.sort() — no key= required, since Python's comparison operators dispatch to these dunder methods directly. functools.total_ordering fills in __le__, __gt__, __ge__ automatically from just __eq__ + __lt__, saving you from writing all four rich-comparison methods by hand.