Multi-Key Sorting with Tuple Keys

Concepts covered: Lambda, Sorting, Splitting & Joining, Tuples, Unpacking

Tuples compare element by element from the left. Python finds the first position where 2 tuples differ and compares only the elements at that position; anything after it is never examined. That rule makes a tuple the natural key for a spec such as 'by artist, then by streams, then by track name': the first slot decides, the second slot only breaks ties in the first, and the third only breaks ties in both.

ComparisonResultDecided at
(1, 'b') < (2, 'a')Trueposition 0, since 1 < 2
(1, 'b') < (1, 'c')Trueposition 1, since 'b' < 'c'
(1, 'b') < (1, 'b', 0)Truethe shorter tuple is a prefix
(False, 9) < (True, 0)Trueposition 0, since False < True

The last row matters later in this section: booleans compare as False < True, so a boolean in the first slot splits rows into 2 groups before any other field is consulted.

Mixed directions: negate the numbers

reverse=True flips the entire key, so it cannot make one slot descend while another ascends. For a numeric field, negate it inside the key: -r['streams'] turns the largest count into the smallest key value. The key (r['artist'], -r['streams'], r['track']) reads as artist from A to Z, most streamed first within each artist, and track name from A to Z when 2 tracks of one artist tie on streams.

catalog = [
    {'artist': 'Nova', 'track': 'Halo', 'streams': 30},
    {'artist': 'Kite', 'track': 'Drift', 'streams': 50},
    {'artist': 'Nova', 'track': 'Ember', 'streams': 45},
    {'artist': 'Nova', 'track': 'Aster', 'streams': 30},
    {'artist': 'Kite', 'track': 'Pulse', 'streams': 50},
]

ordered = sorted(catalog, key=lambda r: (r['artist'], -r['streams'], r['track']))
for r in ordered:
    print(r['artist'], r['streams'], r['track'])
Printed lineKey tupleWhy it lands there
Kite 50 Drift('Kite', -50, 'Drift')Kite sorts before Nova
Kite 50 Pulse('Kite', -50, 'Pulse')ties Drift on slots 0 and 1; 'Drift' < 'Pulse'
Nova 45 Ember('Nova', -45, 'Ember')-45 < -30, so the larger count comes first
Nova 30 Aster('Nova', -30, 'Aster')ties Halo on slots 0 and 1; 'Aster' < 'Halo'
Nova 30 Halo('Nova', -30, 'Halo')last on every slot

Each row of output is explained by the first slot where its key differs from its neighbour's. Working an interview answer this way, slot by slot, catches a wrong sign before the code runs.

Fields that cannot be negated

Negation needs a number. -'Kite' raises TypeError: bad operand type for unary -: 'str', and a datetime.date refuses unary minus in the same way. Three workarounds cover most interview cases:

Missing values with a boolean slot

Real columns contain None, and a single one stops a plain sort. Put a boolean in front of the field: s is None is False for rows with a value and True for rows without, and since False < True, missing values land last. For the numeric slot, -(s or 0) turns a missing value into 0, so None never enters the key at all, and the leading boolean keeps those placeholder zeros apart from real counts. The shorter key (s is None, s) would also work: 2 None slots compare equal with ==, so the comparison moves on to the next slot. A None meets < only when it sits in the first slot that differs, and the boolean in front rules that out.

rows = [
    {'track': 'Halo', 'streams': 30},
    {'track': 'Glow', 'streams': None},
    {'track': 'Drift', 'streams': 50},
    {'track': 'Aura', 'streams': None},
]

def nulls_last_desc(r):
    s = r['streams']
    return (s is None, -(s or 0), r['track'])

result = sorted(rows, key=nulls_last_desc)
print([r['track'] for r in result])

This prints ['Drift', 'Halo', 'Aura', 'Glow']. Drift and Halo carry False and order by descending streams; Aura and Glow carry True, tie on the 0 placeholder, and fall back to track name. To put missing values first instead, lead with s is not None. Writing the key as a named function, as here, also gives a reviewer one place to read the full ordering rule.

About this lesson

This section is part of the Custom Sorting: Beginner lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.