Как структурировать проект на Python: лучшие практики
Основные подходы к управлению проектами на Python
Современный подход с Poetry
Poetry объединяет управление зависимостями, виртуальным окружением и сборкой пакета. Он автоматически создаёт и синхронизирует файл poetry.lock, что гарантирует воспроизводимость окружения.
Как создать новый проект и добавить зависимости?
poetry new my_project
cd my_project
poetry add requestsпроект программа на python (проект программы на python)
Команда poetry new создаёт стандартную структуру:
my_project/
├── pyproject.toml
├── README.rst
├── my_project/
│ └── __init__.py
└── tests/
├── __init__.py
└── test_my_project.py
Для установки зависимостей используется poetry add, для разработки - poetry add --dev pytest. Виртуальное окружение создаётся автоматически при первом запуске.
Типичные проблемы
- Версия Poetry может не соответствовать версии Python в системе - используйте
pipxдля изолированной установки Poetry. - Конфликты зависимостей: Poetry уведомляет о несовместимости версий, требует разрешения вручную.
- При работе с закрытым PyPI-репозиторием требуется настройка
[[tool.poetry.source]].
Цели использования
Подходит для проектов любой сложности, особенно тех, которые в дальнейшем будут опубликованы как пакеты. Обеспечивает чистую изоляцию и простой CI/CD.
Вариант 1: Классический способ - pip + virtualenv
Как создать изолированное окружение и установить зависимости?
python -m venv venv
source venv/bin/activate # или venv\Scripts\activate для Windows
pip install -r requirements.txt
Зависимости фиксируются вручную в requirements.txt, файл блокировок (lock) отсутствует. При обновлении зависимостей разработчик сам решает, какие версии зафиксировать.
Ошибки и их решение
- Случайное использование глобального pip - всегда активируйте виртуальное окружение.
- Различия версий Python между средами - используйте
pyenvдля управления версиями. - Забыли добавить зависимость в
requirements.txt- проверяйте список черезpip freeze.
Когда применять
Для небольших скриптов, прототипов или проектов, где не требуется воспроизводимость на уровне lock-файла.
Вариант 2: Pipenv - автоматизация окружения
Как автоматизировать создание окружения и управление зависимостями?
pipenv install requests
pipenv shell
Pipenv создаёт файлы Pipfile и Pipfile.lock, автоматически активирует окружение при входе в каталог через pipenv shell.
Характерные сложности
- Медленная работа при большом количестве зависимостей.
- Иногда возникают конфликты с уже установленными пакетами в глобальной среде.
- Прекращение активной поддержки Pipenv многими сообществами.
Область применения
Подходит для простых веб-проектов или утилит, где важна простота создания окружения.
Вариант 3: Conda - управление не только Python
Как управлять не только Python, но и системными библиотеками (C/C++, R)?
conda create -n myenv python=3.9 numpy
conda activate myenv
Conda устанавливает пакеты из каналов conda-forge и defaults, разрешая зависимости на уровне бинарных файлов.
Проблемы при использовании
- Большой размер дистрибутива (Miniconda ~500 МБ, Anaconda ~3 ГБ).
- Медленное разрешение зависимостей из‑за поиска по всем каналам.
- Лицензионные ограничения при коммерческом использовании некоторых пакетов.
Сценарии применения
Незаменим для Data Science, машинного обучения, где требуются пакеты с нативным кодом (TensorFlow, CUDA).
Расширенный пример структуры проекта с Poetry
Создадим проект с поддержкой тестирования, линтинга и pre-commit хуков.
poetry new advanced_project
cd advanced_project
poetry add click requests
poetry add --dev pytest pytest-cov black flake8 mypy pre-commit
poetry install
Файл pyproject.toml после добавления зависимостей:
[tool.poetry]
name = "advanced-project"
version = "0.1.0"
description = ""
authors = ["Your Name "]
[tool.poetry.dependencies]
python = "^3.8"
click = "*"
requests = "*"
[tool.poetry.dev-dependencies]
pytest = "*"
pytest-cov = "*"
black = "*"
flake8 = "*"
mypy = "*"
pre-commit = "*"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
Настроим pre-commit. Создадим файл .pre-commit-config.yaml:
repos:
- repo: https://github.com/psf/black
rev: 23.7.0
hooks:
- id: black
- repo: https://github.com/pycqa/flake8
rev: 6.1.0
hooks:
- id: flake8
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.4.1
hooks:
- id: mypy
poetry run pre-commit install # устанавливаем хуки в .git/hooks
Результат работы pre-commit при коммите:
black....................................................................Passed flake8...................................................................Passed mypy.....................................................................Passed
Интеграция с GitHub Actions
Файл .github/workflows/ci.yml для автоматического запуска тестов:
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11"]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install Poetry
run: curl -sSL https://install.python-poetry.org | python3 -
- name: Install dependencies
run: poetry install
- name: Run tests
run: poetry run pytest --cov
Результат в логах CI:
collected 10 items tests/test_advanced_project.py ......... [100%] ----------- coverage: platform linux, python 3.11.0 ----------- Name Stmts Miss Cover -------------------------------------------------------- advanced_project/__init__.py 2 0 100% advanced_project/main.py 15 1 93% tests/test_advanced_project.py 20 0 100% -------------------------------------------------------- TOTAL 37 1 97%
Тестирование нескольких версий Python с помощью tox
Добавим в pyproject.toml секцию [tool.tox]:
[tool.tox]
legacy_tox_ini = """
[tox]
envlist = py39, py310, py311
[testenv]
deps = pytest
commands = pytest
"""
Запуск tox:
poetry run tox
Вывод:
py39 run-test: commands[0] | pytest ============================= test session starts ============================= collected 10 items tests/test_advanced_project.py ......... [100%] ============================== 10 passed in 0.15s ============================= py310 run-test: commands[0] | pytest ... py311 run-test: commands[0] | pytest ... ___________________________________ summary ___________________________________ py39: commands succeeded py310: commands succeeded py311: commands succeeded congratulations :)
Параметризованные тесты с pytest
Пример тестирования функции сложения:
# advanced_project/calculations.py
def add(a, b):
return a + b
# tests/test_calculations.py
import pytest
from advanced_project.calculations import add
@pytest.mark.parametrize("a,b,expected", [
(1, 2, 3),
(0, 0, 0),
(-1, 1, 0),
(2.5, 3.1, 5.6),
])
def test_add(a, b, expected):
assert add(a, b) == expected
Результат:
collected 4 items tests/test_calculations.py .... [100%]