datarekha

Timsort, Stability & sorted()

You will never write a sort from scratch in production — but you need to know exactly what Python's sorted() and list.sort() are doing under the hood, because it changes how you design data pipelines.

6 min read Intermediate Data Structures & Algorithms Lesson 11 of 32

What you'll learn

  • How Timsort combines merge sort and insertion sort to achieve O(n) on nearly-sorted data
  • What stability means and why it matters for multi-key sorts in data science
  • How key= functions work and why they beat a custom comparator every time
  • The two-pass pattern for sorting by multiple fields without a compound key

Before you start

In production Python you will almost never implement a sorting algorithm. You will call sorted() or list.sort(). That is the right call. But knowing what runs underneath — and what stability means — is what separates someone who gets bitten by subtle data bugs from someone who does not.

Timsort: the algorithm you are already using

Both sorted() and list.sort() use Timsort, a hybrid algorithm designed by Tim Peters in 2002 specifically for the kinds of data real programs produce.

The idea in one paragraph: Timsort scans the input for already-sorted subsequences called runs. If a run is short, it extends it using insertion sort (which is fast on small or nearly-sorted data). It then merges those runs using merge sort’s merge step. The result is an algorithm that is:

  • O(n log n) worst case — the same guarantee as merge sort.
  • ~O(n) on already- or nearly-sorted data — because it finds one big run and does almost no merging.

Real-world data is rarely a random shuffle. Logs are nearly time-ordered. A re-sorted leaderboard changes only a few positions. Timsort exploits that structure without you doing anything special.

# Both of these are Timsort — in-place vs returning a new list
scores = [88, 72, 95, 61, 72, 88]

ascending = sorted(scores)       # returns a new list; original unchanged
scores.sort()                    # sorts in place; returns None

# Nearly-sorted — Timsort will detect the existing run and be very fast
almost_sorted = list(range(10_000)) + [10001, 9998, 10000]
almost_sorted.sort()

Stability — what it means and why it matters

A sort is stable if two elements that compare as equal keep their original relative order after sorting. Timsort is stable. This is not an accident; it was a deliberate design requirement.

Why does this matter? Consider a leaderboard with tied scores:

# Input order: Alice appears before Bob, both have score 72
players = [("Alice", 72), ("Carol", 95), ("Bob", 72), ("Dave", 61)]

by_score = sorted(players, key=lambda p: p[1])
# [('Dave', 61), ('Alice', 72), ('Bob', 72), ('Carol', 95)]
#                  ^^^^^^^^^^^  ^^^^^^^^^^
#                  Alice before Bob — input order preserved among ties

Alice appeared before Bob in the input, so she appears before Bob in the output. With an unstable sort that guarantee disappears. That might seem minor until you are building a ranking system where tie-breaking order carries meaning.

The two-pass multi-key trick

Stability is what makes the following pattern possible and correct. To sort by a primary key, then by a secondary key as a tiebreaker:

  1. Sort by the secondary key first.
  2. Stable-sort by the primary key.

Because the second sort is stable, equal primary-key elements retain the order they got in step 1 — which was sorted by secondary key.

This is also how you can replicate multi-key sorting anywhere stability is guaranteed — and it is why stability is a prerequisite for the pattern to be correct. (pandas sort_values(by=["dept", "salary"]) achieves the same result in a single pass using a composite sort key, but the two-pass trick is the pattern to reach for when you need different sort directions per field.)

Run the playground below to see both ideas at once.

key=, reverse=, and why key= beats a comparator

sorted() and list.sort() both accept two optional arguments you will use constantly.

key= takes a callable. Python calls it once per element, stores the result, and sorts by those stored values. The original elements are carried along for the ride. This is the classic decorate-sort-undecorate pattern (also called the Schwartzian transform):

# Sort words by length, then alphabetically within same length
words = ["fig", "apple", "banana", "kiwi", "date"]

# key= is called once per word — O(n) key computations, then O(n log n) comparisons
by_length = sorted(words, key=lambda w: (len(w), w))
print(by_length)
# ['fig', 'date', 'kiwi', 'apple', 'banana']

The alternative — a custom cmp function — was removed in Python 3 because it is strictly worse: a cmp function is called O(n log n) times during sorting, whereas key= computes values just n times up front. For expensive keys (database lookups, regex matches, model inference scores) this difference is significant.

functools.cmp_to_key exists as an escape hatch for cases where a pairwise comparison is truly unavoidable, but those cases are rare.

reverse=True sorts descending without the cost of reversing the list afterward:

# Top-5 scorers
top5 = sorted(players, key=lambda p: p[1], reverse=True)[:5]

You can also negate a numeric key (key=lambda p: -p[1]) for descending order when you need descending on one field and ascending on another in the same pass — though the two-pass approach above is usually clearer.

Quick check

Quick check

0/3
Q1You sort a list of (employee, department, salary) tuples first by salary, then by department. Which property of Python's sort makes the final result correctly ordered by department with salary as a tiebreaker?
Q2A key= function connects to a remote API to fetch a relevance score for each string. Your list has 50,000 strings. How many API calls does sorted(strings, key=fetch_score) make?
Q3On a nearly-sorted list of one million integers, Timsort's practical runtime is closest to which of the following?

Practice this in an interview

All questions
How do you sort a list of dictionaries by a specific key in Python, and what is the difference between sorted() and list.sort()?

Use sorted() with a key= lambda to produce a new sorted list, or list.sort() to sort in place. Both use Timsort and run in O(n log n). sorted() works on any iterable and returns a new list; list.sort() operates in place and returns None.

How do you reverse a list and remove duplicates in Python, and what are the performance implications of each approach?

Reversing a list is O(n) whether you use slice notation or list.reverse(). Deduplication is O(n) with a set conversion but O(n²) if you check membership against a list. Understanding when order must be preserved changes which tool to reach for.

Which Python built-in types are mutable and which are immutable, and why does it matter?

Immutable types — int, float, bool, str, bytes, tuple, frozenset — cannot be changed after creation; operations return new objects. Mutable types — list, dict, set, bytearray — can be changed in place. Mutability determines hashability (only immutables can be dict keys/set members), function side-effect behaviour, and thread-safety considerations.

What is the difference between a list and a tuple, and when should you use each?

Lists are mutable sequences; tuples are immutable. Use a tuple when the collection of items is fixed by meaning — coordinates, RGB values, function return values — and a list when the collection will grow, shrink, or be modified in place. Immutability also makes tuples hashable, so they can serve as dict keys or set members.

Sign in to track your progress

Completed lessons, your XP, level, and streak save to your account — it's free and takes a few seconds.

Explore further

Related lessons

Skip to content