Списки, кортежи и множества в Python: когда что
list, tuple и set решают разные задачи, и выбор влияет на скорость, память и надёжность кода. Разбираем на конкретных операциях, где каждая структура выигрывает и чем платит.

Три структуры — три разные задачи
Проверка if x in collection на списке из миллиона элементов в худшем случае перебирает все элементы. На множестве та же проверка занимает примерно постоянное время. Это не микрооптимизация: на цикле в миллион итераций разница между list и set превращается в разницу между секундами и минутами. Разбираем, когда какая структура уместна — на операциях, которые вы пишете каждый день. Всё ниже актуально для CPython 3.12, но принципы работают начиная с 3.6.
Коротко о сути. Список (list) — изменяемая упорядоченная последовательность. Кортеж (tuple) — неизменяемая упорядоченная последовательность. Множество (set) — изменяемая неупорядоченная коллекция уникальных элементов с быстрой проверкой принадлежности. Дальше — что из этого следует на практике.
Список: когда порядок важен и данные меняются
Список — стандартный выбор, когда вы накапливаете элементы, обходите их по очереди или обращаетесь по индексу. Добавление в конец через append — быстрое (амортизированная константа). Вставка в начало через insert(0, x) — медленная, потому что все остальные элементы сдвигаются вправо.
# CPython 3.12
zakazy = []
zakazy.append({"id": 1, "summa": 1500}) # быстро
zakazy.append({"id": 2, "summa": 2300})
# обход по порядку — то, для чего список создан
itogo = sum(z["summa"] for z in zakazy)
print(itogo) # 3800Если вы часто вставляете и удаляете элементы с обоих концов — списка недостаточно, посмотрите на collections.deque: у него appendleft и popleft работают за константу. Deque — отдельная тема, здесь только отмечаю, что list для очереди с обоих концов не оптимален.
Частая ошибка: проверка in по большому списку
Код вида if user_id in aktivnye_id, где aktivnye_id — список на десятки тысяч элементов, внутри цикла превращается в квадратичную сложность. Разработчик видит «работает» на тесте из десяти записей и не замечает проблему до продакшена.
Множество: уникальность и быстрая проверка принадлежности
Set построен на хеш-таблице. Проверка x in s, добавление и удаление — в среднем за постоянное время независимо от размера. Плата: элементы должны быть хешируемыми (неизменяемыми), порядок не сохраняется, и на хеш-таблицу уходит больше памяти, чем на список из тех же элементов.
# CPython 3.12
# было: медленно, если список большой и проверок много
aktivnye_list = [101, 205, 340, 512] # ... десятки тысяч
if 340 in aktivnye_list: # линейный поиск
pass
# стало: быстрая проверка
aktivnye = set(aktivnye_list)
if 340 in aktivnye: # хеш-поиск, ~константа
passВторой сильный сценарий — операции над множествами. Пересечение, объединение, разность выражаются одной строкой и работают быстрее ручных циклов.
# кто есть в обоих списках
tekushchie = {101, 205, 340}
vchera = {205, 340, 512}
print(tekushchie & vchera) # {340, 205} — пересечение
print(tekushchie | vchera) # объединение
print(tekushchie - vchera) # {101} — только в текущемЕсли вы пишете вложенный цикл, чтобы найти общие элементы двух коллекций, — почти всегда это должно быть пересечение множеств. Одна строка вместо десяти и на порядок быстрее на больших данных.
Дедупликация
Убрать дубликаты из списка — list(set(items)). Но порядок при этом теряется. Если порядок важен, с Python 3.7+ помогает трюк через словарь, который сохраняет порядок вставки:
items = [3, 1, 3, 2, 1]
unique_sorted = list(dict.fromkeys(items))
print(unique_sorted) # [3, 1, 2] — дубли убраны, порядок сохранёнКортеж: неизменяемость как гарантия
Кортеж нужен не для скорости (разница со списком по времени доступа мизерная), а для смысла и надёжности. Неизменяемость даёт три вещи: кортеж можно положить в множество или сделать ключом словаря; его нельзя случайно испортить чужим кодом; он честно сообщает читателю «этот набор фиксирован».
# координата как ключ словаря — список так не сможет
grid = {}
grid[(0, 0)] = "start"
grid[(2, 3)] = "goal"
# список нельзя хешировать:
# grid[[0, 0]] = "start" # TypeError: unhashable type: 'list'Классический пример — возврат нескольких значений из функции. То, что выглядит как return x, y, на самом деле возвращает кортеж.
def razobrat_adres(s: str) -> tuple[str, int]:
host, _, port = s.partition(":")
return host, int(port) # это кортеж (host, port)
host, port = razobrat_adres("example.com:8080")Отдельная тонкость: неизменяемость кортежа поверхностная. Если внутри кортежа лежит список, этот список можно менять. Кортеж гарантирует, что вы не замените элемент целиком, но не замораживает вложенные изменяемые объекты.
Таблица: что выбирать
| Задача | Структура | Почему |
|---|---|---|
| Накапливать и обходить по порядку | list | append за константу, доступ по индексу |
| Частая проверка «есть ли элемент» | set | поиск ~константа против линейного у списка |
| Убрать дубликаты | set | уникальность встроена |
| Пересечение / объединение / разность | set | операции одной строкой |
| Ключ словаря или элемент множества | tuple | хешируемый, неизменяемый |
| Фиксированный набор, менять нельзя | tuple | защита от случайной мутации |
| Очередь с двух концов | deque | list медленный на вставке в начало |
Что ломается на практике
- set не сохраняет порядок. Не полагайтесь на то, что элементы выведутся в порядке вставки — этого гарантии нет. Нужен порядок — используйте
dict.fromkeysили сортируйте явно. - set нельзя вложить в set. Множество изменяемо и потому нехешируемо. Для множества множеств нужен
frozenset— неизменяемая версия. - Кортеж из одного элемента.
(5)— это число 5 в скобках, а не кортеж. Кортеж —(5,)с запятой. Забытая запятая — источник тихих багов. - Мутация во время итерации. Изменять список или множество, пока вы его перебираете в цикле, приводит к пропуску элементов или
RuntimeError. Итерируйте по копии или собирайте изменения в отдельную коллекцию.
Как замерить у себя
Не верьте на слово — проверьте на своих данных модулем timeit из стандартной библиотеки:
import timeit
setup = "data = list(range(100000)); target = 99999"
list_time = timeit.timeit("target in data", setup=setup, number=1000)
setup_set = "data = set(range(100000)); target = 99999"
set_time = timeit.timeit("target in data", setup=setup_set, number=1000)
print(f"list: {list_time:.4f}s, set: {set_time:.4f}s")На поиске отсутствующего или последнего элемента разница видна сразу: список проходит всю коллекцию, множество отвечает мгновенно.
Что делать дальше
Пройдитесь по своему коду и найдите проверки in внутри циклов — если справа список, замените на множество, это самый частый быстрый выигрыш. Дальше посмотрите на collections: deque для очередей, Counter для подсчёта, defaultdict для группировки. Точное поведение и сложность операций — в разделе «Data Structures» официальной документации Python для вашей версии.
Задачи: математика, физика, химия: Разбор задачи по шагам с проверкой и похожей задачей
Спросить за 10 ₽Источники: Python docs — Data Structures, Python docs — Set Types (set, frozenset)
Частые вопросы
А если у меня Python 3.6 или старше — сохраняется ли порядок в dict.fromkeys?
Насколько set быстрее списка на самом деле?
Кортеж действительно быстрее списка?
Почему нельзя положить список в множество?
Что делать, если код падает с RuntimeError при изменении коллекции в цикле?
Когда стоит выбрать frozenset вместо set?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.