Как настроить локальную среду для Python-проекта: venv, pyproject.toml и воспроизводимые зависимости
Разберём, как правильно собрать окружение для разработки и CI: структуры проекта, pyproject.toml, lock-файлы и типовые сценарии воспроизведения зависимостей на другой машине. Параллельно покажем, как избежать “работает у меня”.
Содержание
Как настроить локальную среду для Python-проекта: venv, pyproject.toml и воспроизводимые зависимости
Разработка на Python часто начинается с простого: установить пару библиотек и “побежать”. Но как только проект выходит за рамки личных экспериментов — начинаются неприятные вопросы: почему тесты падают у коллеги, почему в CI другая версия пакета, почему сборка не совпадает с локальной средой. Эти проблемы почти всегда упираются в одно: окружение и зависимости описаны недостаточно строго.
Ниже — практический разбор, как собрать локальную среду разработки и подготовить проект к CI так, чтобы зависимости воспроизводились на другой машине. Мы разберём:
- структуру проекта (что держать в корне, что — в подпапках),
venvкак базовую изоляцию,pyproject.tomlкак источник правды для метаданных и зависимостей,- lock-файлы и их роль,
- типовые сценарии воспроизведения (локально, на CI, на “чистой” машине),
- как избегать эффекта “работает у меня”.
Почему “работает у меня” — системная проблема
В Python проблема воспроизводимости чаще всего появляется в одном из этих сценариев:
-
Зависимости задаются без ограничений по версиям
Например:requestsбез верхней границы. В какой-то момент на машине разработчика стоит одна версия, а на другой — другая, и поведение меняется. -
Указана версия, но не зафиксирован набор транзитивных зависимостей
Даже если вы зафиксировали прямой пакет, его зависимости могут обновиться. Это классический случай, когда “версия вrequirements.txtодна, но всё другое — другое”. -
Локально используются уже установленные пакеты из системы или “случайный” venv
В результате тесты проходят “по инерции”, когда на машине уже стоит нужная версия, но ваш проект это не гарантирует. -
В CI используется отдельная схема установки
Например, локально вы ставили черезpip, а в CI — через другую команду/параметры, или вообще забыли использовать lock-файл.
Итог: окружение становится недетерминированным. Поэтому в корректной настройке “истина” должна быть описана в репозитории и однозначно воспроизводиться.
Архитектура проекта: что и где хранить
Прежде чем писать конфигурации, стоит определить “формат” репозитория. Ниже — типичная структура, которая хорошо работает для Python-проектов (и в dev, и в CI):
pyproject.toml— метаданные, зависимости, конфигурации инструментов.src/— исходники (рекомендуемый layout для избежания конфликтов импортов).tests/— тесты.README.md, возможноLICENSE.- lock-файл (имя зависит от инструмента, но обычно это
uv.lock,poetry.lock,Pipfile.lockили файл дляpip-tools). .venv/в корне — локально, но в.gitignore.
Пример структуры:
my-project/
├─ pyproject.toml
├─ README.md
├─ src/
│ └─ my_project/
│ ├─ __init__.py
│ └─ app.py
├─ tests/
│ └─ test_app.py
├─ .gitignore
├─ .github/
│ └─ workflows/
│ └─ ci.yml
└─ uv.lock (или другой lock-файл)
Почему src/ layout полезен
Если проект лежит “плоско” (например, my_project/ в корне), то в некоторых сценариях интерпретатор может подхватить модуль из текущей рабочей директории, а не из установленного пакета. src/ layout обычно снижает вероятность таких “магических” совпадений.
venv: базовая изоляция для локальной разработки
venv не решает вопрос воспроизводимости зависимостей сам по себе, но он обеспечивает предсказуемость окружения на уровне Python и установленного набора пакетов. Практика такая:
- Создать виртуальное окружение.
- Установить зависимости строго из lock-файла/воспроизводимой схемы.
- Использовать это окружение для запуска тестов, линтера и т.п.
Создание venv
На Unix/macOS:
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
На Windows (PowerShell):
py -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
Что добавить в .gitignore
# virtual environments
.venv/
venv/
__pycache__/
*.pyc
pyproject.toml: где описывать зависимости и инструментальную конфигурацию
Сегодня “правильный” путь — использовать pyproject.toml как единый файл настроек: зависимости, build-система и часто конфигурации инструментов (линтеры, форматтеры, типизация). Плюс — это стандарт, вокруг которого выстраиваются современные инструменты.
Ниже пример структуры pyproject.toml в современном стиле (минимально, но по делу):
[build-system]
requires = ["hatchling>=1.18"]
build-backend = "hatchling.build"
[project]
name = "my-project"
version = "0.1.0"
description = "Example Python project"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"requests>=2.31,<3.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0,<9.0",
"ruff>=0.5,<1.0",
"mypy>=1.10,<2.0",
]
[tool.pytest.ini_options]
testpaths = ["tests"]
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.mypy]
python_version = "3.11"
На что обратить внимание
-
requires-pythonфиксирует диапазон версий интерпретатора
Это важно: если CI использует другую версию Python, поведение может отличаться. Лучше явно задать минимум/диапазон. -
Диапазоны версий вместо “свободных” зависимостей
Практический баланс: для большинства пакетов ставят нижнюю границу (чтобы избежать старых багов) и верхнюю границу (чтобы не попасть в несовместимую мажорную версию).
Например:requests>=2.31,<3.0. -
Optional dependencies (
dev,test,docs)
Они отделяют зависимости для разработки от runtime. Это упрощает воспроизведение и уменьшает шум.
Lock-файл: ключ к воспроизводимым зависимостям
Если pyproject.toml описывает желаемое, то lock-файл описывает конкретное решение резолвера — версии прямых и транзитивных зависимостей.
Почему lock-файл необходим даже с верхними границами
Даже при requests>=2.31,<3.0 вы можете получить разные версии на разных машинах в разное время. Lock гарантирует, что набор останется одинаковым, пока вы не выполните обновление осознанно.
Какие lock-файлы бывают
Зависит от инструмента установки. На практике встречаются варианты:
uv.lock— если используетеuv(быстрый менеджер зависимостей).poetry.lock— если используете Poetry.requirements.txt+pip-tools(.txt-реплика) — если используетеpip-tools.Pipfile.lock— если используете pipenv.
В этой статье сосредоточимся на подходе “через инструмент с lock-файлом”, не привязываясь жестко к одному бренду. Однако ниже я дам рабочие примеры под uv, потому что он довольно часто встречается в современных пайплайнах.
Вариант с uv: pyproject.toml + uv.lock + установка из lock-файла
uv умеет читать pyproject.toml, резолвить зависимости и фиксировать результат в uv.lock. Он ускоряет работу по сравнению с “чистым” pip, но важнее не скорость, а предсказуемость.
Инициализация lock-файла
В корне проекта:
uv sync --lock-only
Если uv ещё не установлен:
pip install uv
После этого появится uv.lock.
Установка зависимостей локально
Чтобы установить строго то, что в lock-файле:
uv sync
или с учётом окружения разработки (dev-зависимости):
uv sync --extra dev
Когда обновлять зависимости
Обновление делается отдельной командой, чтобы изменения зависимостей были явными в PR:
uv lock
или если хотите обновить с контролем селективности — инструменты обычно позволяют это делать (зависит от версии uv и настроек). Суть такая: lock меняется осознанно, а не “само”.
Вариант с Poetry (кратко): poetry.lock и предсказуемая установка
Если ваш проект использует Poetry, логика та же:
- зависимости описываются в
pyproject.toml(в разделе[tool.poetry.dependencies]), - фиксируются в
poetry.lock, - установка происходит из lock-файла командой
poetry install --no-root.
С практической точки зрения различается формат файла и некоторые детали, но принцип воспроизводимости остаётся тем же: lock-файл должен попадать в репозиторий, а CI и локальная машина должны опираться на него.
Воспроизведение на “другой машине”: пошаговый сценарий
Давайте смоделируем реальный кейс: вы отправили репозиторий человеку, у которого нет зависимостей вашего проекта. Его шаги должны быть предсказуемыми.
Сценарий A: “Новая машина”, Python уже установлен
- Клонировать репозиторий.
- Создать venv.
- Установить зависимости строго из lock-файла.
- Запустить тесты.
Пример команд (под uv):
git clone https://example.com/my-project.git
cd my-project
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install uv
uv sync --extra dev
pytest -q
Если в проекте установлен pre-commit/линтер — их тоже запускают из этого же окружения.
Сценарий B: другой Python версии
Если в pyproject.toml задан requires-python = ">=3.11", то попытка поставить зависимости в Python 3.10 приведёт к ошибке. Это хорошо: проект честно сообщает ограничения.
В CI стоит также явно выбрать версию Python, например 3.11 и/или 3.12.
Подготовка CI: чтобы локальное и “серверное” совпадали
CI — это не только прогон тестов. Это проверка гипотезы: “окружение из репозитория действительно работает”.
Минимальные требования к CI
- использовать такой же Python (или набор версий, которые вы поддерживаете),
- установить зависимости из lock-файла,
- не допускать “подмешивания” глобальных пакетов.
Пример GitHub Actions (универсальный)
Ниже демонстрационный workflow, ориентированный на uv и uv.lock.
name: CI
on:
push:
pull_request:
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install uv
run: |
python -m pip install --upgrade pip
pip install uv
- name: Sync dependencies (from lock)
run: |
uv sync --extra dev
- name: Run tests
run: |
pytest -q
Важно: uv sync должен опираться на lock-файл
Если lock отсутствует, инструмент может пойти в “резолв заново”, и воспроизводимость сломается. Поэтому в процессе разработки рекомендуется держать lock в репозитории и обновлять его осознанно.
Как избежать типовых ошибок (и почему они возникают)
Ошибка 1: запускать команды без активации venv
Вроде мелочь, но это частая причина “у меня всё работает”. Проверка:
which python
python -V
pip -V
Если python указывает не на .venv, вы не в том окружении.
Ошибка 2: не фиксировать транзитивные зависимости
Фиксировать только прямые зависимости — недостаточно. Lock-файл сохраняет полный набор решений резолвера.
Ошибка 3: обновлять зависимости “вручную” и забывать закоммитить lock
Если вы обновляете пакеты через pip install -U, lock не изменится (или будет рассинхрон). В идеале: обновление делается через инструмент, который управляет lock.
Ошибка 4: использовать разные способы установки локально и в CI
Например:
- локально ставите
pip install -r requirements.txt, - в CI — через
poetry installилиpip install ., - или локально используете
--no-deps.
Разница в стратегии приводит к тому, что одна среда “случайно” совпала, а другая — нет. CI должен повторять то, что вы считаете “официальной” установкой.
Ошибка 5: отсутствие проверки на чистом окружении
Даже с lock-файлом стоит тестировать “с нуля”: удалить .venv, создать заново, установить зависимости из lock и прогнать тесты. Это самый быстрый способ обнаружить, что вы опираетесь на уже установленные пакеты (чаще всего — инструменты разработки).
Разработка: удобные команды, которые не ломают воспроизводимость
Обычно проект требует часто запускать похожие команды: тесты, линтеры, форматтер, типизацию. Чтобы не плодить “магические” зависимости в документации, удобно:
- запускать все инструменты из одного окружения,
- хранить команды в
Makefileили вscripts/, - при необходимости — фиксировать версии в
devoptional-dependencies.
Пример: Makefile для единообразия
.PHONY: test lint format type
test:
pytest -q
lint:
ruff check .
format:
ruff format .
type:
mypy src
Команды не “знают” про venv. Они предполагают, что вы активировали окружение или что CI его настроил.
Стратегия управления зависимостями: когда и как обновлять
Воспроизводимость — это не запрет обновлений. Это управление циклом обновлений:
- Регулярно (например, раз в месяц) обновляете lock-файл.
- Проверяете тестами и линтерами.
- Если обновление крупное — разделяете PR по зонам влияния (например, runtime vs dev).
Важно понимать: обновление зависимостей — это изменение входных данных для тестов. Поэтому чем лучше тесты покрывают поведение, тем меньше рисков “тихих” регрессий.
Мини-чеклист: “правильная” локальная среда и воспроизводимость
Перед тем как считать систему стабильной, проверьте:
- В репозитории есть
pyproject.toml, описывающий зависимости (runtime и dev). - В репозитории есть lock-файл (например,
uv.lock/poetry.lock/аналог). - Локально вы устанавливаете зависимости из lock-файла, а не “по минимуму и потом докинуть”.
- CI устанавливает зависимости тем же образом и с тем же lock-файлом.
-
.venv/не коммитится в репозиторий, но в README есть инструкция “как поднять окружение”. - Были прогнаны тесты на чистом окружении (fresh venv).
- Версии Python согласованы между
requires-pythonи CI.
Какой подход выбрать: один принцип, разные инструменты
Существует несколько “правильных” путей: Poetry, uv, pip-tools и т.д. Они отличаются синтаксисом и workflow, но объединены одной идеей:
pyproject.toml— описание намерений,- lock-файл — фиксация результата резолва,
- CI и локальная разработка — должны использовать один и тот же источник истины.
Если вы хотите глубже разобраться в том, как устроены современные практики упаковки и управления зависимостями в Python, полезно посмотреть разборы по теме в образовательных материалах, например в курсах на /course/ — как дополнительный способ структурировать знания и увидеть больше примеров на реальных проектах.
Вывод: воспроизводимость — это дисциплина описания, а не магия инструментов
“Работает у меня” возникает не из-за того, что Python плох, а из-за того, что окружение описано неполно. Надёжная локальная среда для Python-проекта строится вокруг трёх опор:
- Изоляция через
venv— чтобы локальные пакеты не влияли на воспроизводимость. - Описания в
pyproject.toml— как единый контракт по зависимостям и совместимости. - Lock-файл и установка из него
Комментарии
Пока нет комментариев