requirements.txt построчно: как pip читает каждую строку
Один и тот же requirements.txt на двух машинах ставит разные версии, а сборка в CI падает там, где локально всё зелёно. Разбираем, что pip делает с каждой строкой файла и почему.

Файл из двух строк django и requests кажется безобидным ровно до того момента, пока сборка в CI не потянет Django 5.1, а локально у вас стоит 4.2. Или пока pip install -r requirements.txt не упадёт с ResolutionImpossible на конфликте транзитивных зависимостей. Формат requirements.txt выглядит тривиально, но каждый символ в строке меняет то, что окажется в окружении. Разбираем строку за строкой: спецификаторы версий, режим editable, ссылки на VCS, хеши и вложенные файлы — на pip 24.x и Python 3.11+.
Что вообще читает pip
requirements.txt — это не формат pip в узком смысле, а список аргументов, которые вы могли бы передать pip install руками, только по одному на строку. Поэтому строка requests==2.32.3 эквивалентна pip install requests==2.32.3. Это объясняет большинство «странностей»: всё, что работает в командной строке, работает и в файле.
Комментарии начинаются с # и идут до конца строки. Пустые строки игнорируются. Обратный слэш в конце строки склеивает её со следующей — иногда полезно для длинных строк с хешами.
Строка с версией: спецификаторы
Самая частая строка — имя пакета и ограничение версии. Ограничение задаётся операторами из спецификации version specifiers. Разница между ними — это разница между воспроизводимой сборкой и лотереей.
requests==2.32.3 # ровно эта версия
requests>=2.31 # не ниже 2.31, потолка нет
requests~=2.31.0 # совместимая: >=2.31.0, <2.32.0
requests!=2.32.0 # любая, кроме этой
django>=4.2,<5.0 # диапазон через запятую
Оператор ~= (compatible release) читается неочевидно. ~=2.31.0 разрешает патч-версии внутри 2.31, но не 2.32. А ~=2.31 (без третьего числа) разрешает любую 2.x, начиная с 2.31. Правило: ~= фиксирует все части версии, кроме последней указанной.
Строка без оператора версии — requests — означает «любая версия, какая найдётся». В приложении это почти всегда ошибка: сборка перестаёт быть воспроизводимой в тот день, когда автор пакета выпустит мажорную версию с ломающими изменениями.
Extras — квадратные скобки
Некоторые пакеты выносят часть зависимостей в опциональные группы. Их подтягивают через квадратные скобки:
celery[redis]==5.4.0 # celery плюс зависимости для redis-брокера
httpx[http2]>=0.27
uvicorn[standard]==0.30.6 # uvicorn с ускорителями и watchfiles
Что именно входит в extra, определяет сам пакет в своих метаданных — список фиксированных имён нигде не выдумывается, он берётся из документации пакета.
Строки, которые ставят не из PyPI
Editable-установка
Флаг -e ставит пакет в режиме редактирования: код не копируется в site-packages, а подключается симлинком на исходники. Изменения в файлах видны без переустановки. Типично для монорепо и локальной разработки библиотеки.
-e ./libs/mypackage # локальный путь
-e . # текущий проект
В requirements.txt для продакшена editable-строк быть не должно: они предполагают наличие исходников рядом, которых на сервере нет.
Установка из Git
Пакет можно тянуть прямо из репозитория. Синтаксис жёсткий, и порядок частей важен:
git+https://github.com/user/repo.git@v1.2.0#egg=mypackage
git+https://github.com/user/repo.git@a1b2c3d#egg=mypackage
После @ идёт тег, ветка или хеш коммита. Ветка вроде @main снова делает сборку невоспроизводимой — pip возьмёт то, что лежит в ветке на момент установки. Для воспроизводимости указывайте тег или полный хеш коммита.
Путь к файлу или каталогу
./dist/mypackage-1.0.0-py3-none-any.whl # локальный wheel
/opt/wheels/somepkg-2.1.0.tar.gz # локальный sdist
Управляющие строки
Некоторые строки не ставят пакет, а меняют поведение pip для всего файла.
| Строка | Что делает |
|---|---|
-r base.txt | Включает другой файл требований. Так делят dev и prod |
-c constraints.txt | Подключает файл ограничений: версии оттуда применяются, но пакеты сами по себе не ставятся |
--index-url https://... | Меняет основной индекс пакетов (например, приватный PyPI) |
--extra-index-url https://... | Добавляет ещё один индекс к основному |
--hash=sha256:... | Привязывает пакет к контрольной сумме дистрибутива |
Разница между -r и -c ловит многих. -r означает «установи всё это тоже». -c означает «если что-то из этого будет ставиться как зависимость, используй ровно эту версию, но сам по себе ничего не ставь». Constraints-файл — способ зафиксировать транзитивные зависимости, не перечисляя их как прямые.
Хеши и режим hash-checking
Как только у хотя бы одной строки появляется --hash, pip переходит в режим проверки хешей для всего файла: каждый пакет, включая транзитивные, обязан иметь хеш, иначе установка падает. Это защита от подмены дистрибутива на индексе.
requests==2.32.3 \
--hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
Руками такие файлы не пишут. Их генерирует pip-compile из пакета pip-tools или экспорт из другого менеджера — маркер маркер того, что requirements.txt здесь производный артефакт, а не редактируемый вручную.
Маркеры окружения
После точки с запятой можно поставить условие, при котором строка применяется. Это спасает, когда часть зависимостей нужна только на определённой платформе или версии Python.
backports.zoneinfo; python_version < "3.9"
pywin32==306; sys_platform == "win32"
Полный набор переменных маркеров (python_version, sys_platform, platform_machine и другие) задан в спецификации dependency specifiers.
Порядок разбора и приоритеты
- pip читает файл сверху вниз и собирает все требования в один набор до начала установки. Порядок строк на итоговые версии почти не влияет — работает резолвер.
- Резолвер (в pip 20.3+ он новый, backtracking) ищет набор версий, удовлетворяющий сразу всем ограничениям — прямым и транзитивным.
- Если совместимого набора нет, вы получаете
ResolutionImpossibleс деревом конфликтующих требований.
Дублирование пакета с разными версиями в одном файле —django==4.2и нижеdjango==5.0— не «победит последний». pip увидит противоречие и откажется ставить оба.
Если не получилось
ERROR: Could not find a version that satisfies the requirement X— опечатка в имени, пакета нет на текущем индексе или ваш Python слишком старый/новый для доступных wheel. Проверьте имя на PyPI и версию Python.ResolutionImpossible— два пакета требуют несовместимые версии общей зависимости. pip выведет, кто чего хочет. Решение — ослабить одно из ограничений или закрепить конфликтную зависимость через constraints-файл.In --require-hashes mode, all requirements must have their versions pinned with ==— включился режим хешей, а где-то версия не зафиксирована через==или отсутствует хеш. Либо уберите хеши, либо догенерируйте их через pip-tools.- Ставится не та версия, что в файле — почти всегда где-то есть
--extra-index-urlс более свежим пакетом или кеш. Попробуйтеpip install --no-cache-dirи проверьте, какие индексы подключены.
Что делать дальше
Ручной requirements.txt плохо масштабируется: вы фиксируете прямые зависимости, но не транзитивные, и воспроизводимость дырявая. Следующий шаг — разнести файлы на входные (requirements.in с версиями по вкусу) и скомпилированные (requirements.txt с полным закреплением и хешами), а компиляцию отдать pip-tools. Для новых проектов посмотрите на uv или Poetry с lock-файлом — они решают ту же задачу воспроизводимости строже. Что мерить в CI: время холодной установки и совпадение хешей между сборками — расхождение означает, что где-то версия уплыла.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: pip — Requirements File Format, Python Packaging — Version Specifiers
Частые вопросы
Чем ~= отличается от >= на практике?
А если у меня другая версия pip — синтаксис отличается?
Почему у меня на двух машинах ставятся разные версии из одного файла?
Что будет, если один пакет указан в файле дважды с разными версиями?
Нужно ли писать хеши руками?
Можно ли ставить пакет из приватного репозитория Git?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.