Build (cat, score, name) tuples from each record and sort them with Python's tuple comparison, which compares left-to-right. With pandas, passing a list of column names to sort_values sorts hierarchically — primary key first, secondary key as a tiebreaker.

By hand

Build pairs as a list of (cat, score, name) tuples — one per row. Call pairs.sort(), which compares tuples left-to-right: first by cat, then by score within the same category. Extract names from the sorted pairs.

naive.py
Replay: real traced execution (multi-file project)
# trace: ignore records
records = [
    {'name': 'al', 'cat': 'B', 'score': 80},
    {'name': 'bo', 'cat': 'A', 'score': 55},
    {'name': 'cy', 'cat': 'B', 'score': 70},
    {'name': 'di', 'cat': 'A', 'score': 90},
]
pairs = []
for r in records:
    pairs.append((r['cat'], r['score'], r['name']))
pairs.sort()
result = [p[2] for p in pairs]
print('RESULT:', result)
  1. {'name': 'al', 'cat': 'B', 'score': 80},

    2records = [3    {'name': 'al', 'cat': 'B', 'score': 80},4    {'name': 'bo', 'cat': 'A', 'score': 55},
  2. {'name': 'bo', 'cat': 'A', 'score': 55},

    3{'name': 'al', 'cat': 'B', 'score': 80},4{'name': 'bo', 'cat': 'A', 'score': 55},5{'name': 'cy', 'cat': 'B', 'score': 70},
  3. {'name': 'cy', 'cat': 'B', 'score': 70},

    4{'name': 'bo', 'cat': 'A', 'score': 55},5{'name': 'cy', 'cat': 'B', 'score': 70},6{'name': 'di', 'cat': 'A', 'score': 90},
  4. {'name': 'di', 'cat': 'A', 'score': 90},

    5    {'name': 'cy', 'cat': 'B', 'score': 70},6    {'name': 'di', 'cat': 'A', 'score': 90},7]
  5. records = [

    1# trace: ignore records2records = [3    {'name': 'al', 'cat': 'B', 'score': 80},
  6. pairs ← []

    7]8pairs = []9for r in records:
    values this step[]pairs
  7. r ← {'name': 'al', 'cat': 'B', 'score': 80}

    8pairs = []9for r in records:10    pairs.append((r['cat'], r['score'], r['name']))
    values this step{'name': 'al', 'cat': 'B', 'score': 80}r
  8. pairs ← [('B', 80, 'al')]

    9for r in records:10    pairs.append((r['cat'], r['score'], r['name']))11pairs.sort()
    values this step[] [('B', 80, 'al')]pairs
  9. r ← {'name': 'bo', 'cat': 'A', 'score': 55}

    8pairs = []9for r in records:10    pairs.append((r['cat'], r['score'], r['name']))
    values this step{'name': 'al', 'cat': 'B', 'score': 80} {'name': 'bo', 'cat': 'A', 'score': 55}r
  10. pairs ← [('B', 80, 'al'), ('A', 55, 'bo')]

    9for r in records:10    pairs.append((r['cat'], r['score'], r['name']))11pairs.sort()
    values this step[('B', 80, 'al')] [('B', 80, 'al'), ('A', 55, 'bo')]pairs
  11. r ← {'name': 'cy', 'cat': 'B', 'score': 70}

    8pairs = []9for r in records:10    pairs.append((r['cat'], r['score'], r['name']))
    values this step{'name': 'bo', 'cat': 'A', 'score': 55} {'name': 'cy', 'cat': 'B', 'score': 70}r
  12. pairs ← [('B', 80, 'al'), ('A', 55, 'bo'), ('B', 70, 'cy')]

    9for r in records:10    pairs.append((r['cat'], r['score'], r['name']))11pairs.sort()
    values this step[('B', 80, 'al'), ('A', 55, 'bo')] [('B', 80, 'al'), ('A', 55, 'bo'), ('B', 70, 'cy')]pairs
  13. r ← {'name': 'di', 'cat': 'A', 'score': 90}

    8pairs = []9for r in records:10    pairs.append((r['cat'], r['score'], r['name']))
    values this step{'name': 'cy', 'cat': 'B', 'score': 70} {'name': 'di', 'cat': 'A', 'score': 90}r
  14. pairs ← [('B', 80, 'al'), ('A', 55, 'bo'), ('B', 70, 'cy'), ('A', 90, 'di')]

    9for r in records:10    pairs.append((r['cat'], r['score'], r['name']))11pairs.sort()
    values this step[('B', 80, 'al'), ('A', 55, 'bo'), ('B', 70, 'cy')] [('B', 80, 'al'), ('A', 55, 'bo'), ('B', 70, 'cy'), ('A', 90, 'di')]pairs
  15. for r in records:

    8pairs = []9for r in records:10    pairs.append((r['cat'], r['score'], r['name']))
  16. pairs ← [('A', 55, 'bo'), ('A', 90, 'di'), ('B', 70, 'cy'), ('B', 80, 'al')]

    10    pairs.append((r['cat'], r['score'], r['name']))11pairs.sort()12result = [p[2] for p in pairs]
    values this step[('B', 80, 'al'), ('A', 55, 'bo'), ('B', 70, 'cy'), ('A', 90, 'di')] [('A', 55, 'bo'), ('A', 90, 'di'), ('B', 70, 'cy'), ('B', 80, 'al')]pairs
  17. result ← ['bo', 'di', 'cy', 'al']

    11pairs.sort()12result = [p[2] for p in pairs]13print('RESULT:', result)
    values this step['bo', 'di', 'cy', 'al']result
  18. stdout ← RESULT: ['bo', 'di', 'cy', 'al']

    12result = [p[2] for p in pairs]13print('RESULT:', result)
    values this stepRESULT: ['bo', 'di', 'cy', 'al']stdout

With pandas

df.sort_values(['cat', 'score']) sorts by cat first, using score to break ties within each category. The snapshot shows all three columns in their final sorted order.

library.py
import pandas as pd
from dalib.display import set_display
set_display()

records = [
    {'name': 'al', 'cat': 'B', 'score': 80},
    {'name': 'bo', 'cat': 'A', 'score': 55},
    {'name': 'cy', 'cat': 'B', 'score': 70},
    {'name': 'di', 'cat': 'A', 'score': 90},
]
df = pd.DataFrame(records)
sorted_df = df.sort_values(['cat', 'score'])
print('cats:', sorted_df['cat'].tolist())
print('scores:', sorted_df['score'].tolist())
print('names:', sorted_df['name'].tolist())
print('RESULT:', sorted_df['name'].tolist())
cats: ['A', 'A', 'B', 'B']
scores: [55, 90, 70, 80]
names: ['bo', 'di', 'cy', 'al']
RESULT: ['bo', 'di', 'cy', 'al']

Implementation notes

  • The list of column names is sorted hierarchically left-to-right: first column is primary, second is the tiebreaker, and so on. This mirrors how Python tuple comparison works — the naive half uses the same rule explicitly.
  • Control direction per column with a matching list: df.sort_values(['cat', 'score'], ascending=[True, False]) sorts categories ascending but scores descending within each category.
  • sort_values defaults to kind='quicksort', which is not guaranteed stable. Pass kind='stable' (or kind='mergesort') to preserve the original relative order of rows with equal keys, matching Python's list.sort() guarantee.
  • Cross-reference: sort-two-keys (python-data-basics) for the pure-Python tuple-sort version.