Сортировка по нескольким ключам в Python: частые ошибки
Как отсортировать список словарей сразу по нескольким полям и не наступить на грабли с обратным порядком, None и смешанными типами. Разбор на рабочих примерах для Python 3.x.

Вы получите список словарей, отсортированный сначала по одному полю, потом по второму, при этом одно поле — по возрастанию, другое — по убыванию. На это уйдёт минут пять, если знать, как ведёт себя sorted с кортежами. Всё, что ниже, работает на любой актуальной версии Python 3.x.
Что нужно знать до начала
- Python 3.x (проверялось на 3.11 и 3.12, но приёмы стабильны с 3.0).
- Понимание разницы между
sorted()— возвращает новый список — и методомlist.sort(), который меняет список на месте и возвращаетNone. - Данные для примеров: список сотрудников со словарями.
people = [
{"name": "Анна", "dept": "IT", "salary": 120000, "age": 34},
{"name": "Борис", "dept": "HR", "salary": 90000, "age": 41},
{"name": "Вера", "dept": "IT", "salary": 120000, "age": 29},
{"name": "Глеб", "dept": "HR", "salary": 95000, "age": 41},
]
Базовый приём: кортеж в ключе
Сортировка по нескольким полям в Python — это сортировка по кортежу. Функция в key должна вернуть кортеж, а Python сравнит его поэлементно: сначала первый элемент, при равенстве — второй, и так далее.
# Сначала по отделу, внутри отдела — по имени
result = sorted(people, key=lambda person: (person["dept"], person["name"]))
for p in result:
print(p["dept"], p["name"])
# HR Борис
# HR Глеб
# IT Анна
# IT Вера
Кортеж (person["dept"], person["name"]) — единственное, что нужно понять. Всё остальное в этой теме — вариации вокруг него.
Ошибка 1: сортировать в два вызова и ждать чуда
Новички часто пишут два отдельных sort, надеясь, что первый «запомнится»:
# Так делать НЕ надо для независимых ключей
people.sort(key=lambda p: p["name"])
people.sort(key=lambda p: p["dept"])
Здесь есть тонкость, о которой важно знать: сортировка в Python устойчивая (stable) — элементы с равными ключами сохраняют исходный порядок. Поэтому два последовательных sort в обратном порядке ключей на самом деле дают верный результат: последним применяется главный ключ. Но читается это плохо, а при добавлении третьего ключа легко перепутать порядок вызовов. Один кортеж понятнее.
Разные направления: возрастание и убывание одновременно
Вот где ломается большинство. Флаг reverse=True переворачивает весь результат, а не отдельное поле. Нельзя сказать «этот ключ по возрастанию, а этот по убыванию» через один reverse.
Задача: отдел по алфавиту (возрастание), а внутри отдела зарплата по убыванию.
Способ 1: минус перед числом
Для числовых полей самый чистый приём — поставить минус:
result = sorted(
people,
key=lambda p: (p["dept"], -p["salary"])
)
for p in result:
print(p["dept"], p["salary"])
# HR 95000
# HR 90000
# IT 120000
# IT 120000
Минус работает только с числами. Для строк -p["name"] выбросит TypeError.
Способ 2: устойчивая сортировка в два прохода
Когда одно из полей — строка и минус не применить, используйте устойчивость. Сортируйте сначала по второстепенному ключу с его направлением, затем по главному:
# Главный ключ — отдел (возр.), второстепенный — имя (убыв.)
result = sorted(people, key=lambda p: p["name"], reverse=True)
result = sorted(result, key=lambda p: p["dept"]) # reverse не нужен
for p in result:
print(p["dept"], p["name"])
Порядок вызовов обратный порядку приоритета: последний sorted задаёт главный ключ. Это единственный надёжный способ развернуть строковое поле, не трогая остальные.
reverse переворачивает весь результат целиком. Если вам нужны разные направления для разных полей — либо минус для чисел, либо два устойчивых прохода. Третьего простого пути нет.
Когда что выбирать
| Ситуация | Приём | Ограничение |
|---|---|---|
| Все поля в одну сторону | Кортеж в key, при нужде reverse=True на всё | — |
| Разные направления, поле числовое | Минус перед значением в кортеже | Только числа |
| Разные направления, поле строковое | Два прохода sorted, полагаясь на устойчивость | Два вызова, читать порядок наоборот |
| Много полей, чистый код | operator.itemgetter | Не даёт минус и разные направления сразу |
itemgetter вместо lambda
Для нескольких простых ключей operator.itemgetter читается лучше и работает чуть быстрее, потому что это встроенный C-объект, а не Python-функция:
from operator import itemgetter
result = sorted(people, key=itemgetter("dept", "name"))
Минус: itemgetter не умеет переворачивать отдельное поле. Как только нужны разные направления — возвращайтесь к lambda с минусом или к двум проходам.
Что ломается
None в поле
Если хоть в одном словаре значение ключа — None, сравнение падает:
# TypeError: '<' not supported between instances of 'NoneType' and 'int'
Подставьте значение по умолчанию прямо в ключе. Для чисел, где None должен уходить в конец при возрастании, подойдёт float("inf"):
result = sorted(
people,
key=lambda p: (p["dept"], p.get("salary") or 0)
)
Осторожно с or 0: реальный ноль тоже заменится на ноль — это безопасно, а вот для строк or "" отправит пустые в начало, что не всегда нужно.
Смешанные типы в одном поле
Если в одном поле встречаются и числа, и строки, сравнение упадёт с тем же TypeError. Python 3 не сравнивает int со str. Приведите поле к одному типу до сортировки или нормализуйте ключ: str(p["code"]).
Забытый возврат кортежа
Классика — вернуть из lambda не кортеж, а список или два значения через запятую без скобок в неожиданном месте. Список тоже сравнивается поэлементно и сработает, но кортеж принято использовать как неизменяемый ключ. Держитесь скобок: (a, b).
Регистр строк
Строки сравниваются по кодам символов, поэтому все заглавные идут раньше строчных: "Я" < "а" по Unicode. Если нужен алфавитный порядок без учёта регистра — приведите к нижнему регистру в ключе: p["name"].lower().
Что делать дальше
Если сортируете большие объёмы или горячий путь — замерьте itemgetter против lambda на своих данных через timeit; на десятках тысяч элементов разница уже заметна. Если данные приходят из базы, разумнее сортировать в SQL через ORDER BY field1 ASC, field2 DESC, а не тянуть всё в память. Для сложных естественных сортировок (числа внутри строк, версии) смотрите сторонние библиотеки вроде natsort — встроенный sorted их не покрывает.
Разбор научной статьи: Сложный текст — понятным языком, с терминами
Спросить за 12 ₽Источники: Python HOWTO: Sorting Techniques, operator — Standard operators as functions
Частые вопросы
Почему reverse=True не переворачивает только одно поле?
sort() вернул None, это баг?
А если у меня старая версия Python, приёмы сработают?
Что делать, если сортировка падает с TypeError про NoneType?
Как сортировать без учёта регистра?
Два прохода sorted не медленнее одного с кортежем?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.