MidТеория4 min

Структура Python-проекта

Организация кода: src layout, flat layout, setuptools, Poetry, uv, entry_points и best practices

Правильная структура проекта упрощает разработку, тестирование и распространение кода. Python предлагает несколько подходов к организации, и выбор между ними зависит от масштаба проекта и требований.

Два основных подхода

Flat layout

Пакет находится в корне проекта. Простой, подходит для небольших проектов:

myproject/
    mypackage/
        __init__.py
        core.py
        utils.py
    tests/
        test_core.py
        test_utils.py
    pyproject.toml
    README.md

Src layout

Пакет лежит внутри директории src/. Рекомендуется для библиотек и серьезных проектов:

myproject/
    src/
        mypackage/
            __init__.py
            core.py
            utils.py
    tests/
        test_core.py
        test_utils.py
    pyproject.toml
    README.md

Почему src layout лучше

Src layout имеет важное преимущество: при запуске тестов Python не может случайно импортировать пакет из текущей директории вместо установленной версии. Это предотвращает целый класс труднодиагностируемых ошибок.

# With flat layout:
# python -m pytest
# Python might import from ./mypackage/ instead of installed version

# With src layout:
# python -m pytest
# Python can only import the INSTALLED version
# ./src/mypackage/ is not on sys.path

Полная структура проекта

Для серьезного проекта структура выглядит так:

myproject/
    src/
        mypackage/
            __init__.py
            __main__.py        # Entry point: python -m mypackage
            core/
                __init__.py
                models.py
                services.py
            api/
                __init__.py
                routes.py
                middleware.py
            utils/
                __init__.py
                validators.py
                helpers.py
            config.py
            exceptions.py
    tests/
        __init__.py
        conftest.py            # Shared pytest fixtures
        unit/
            test_models.py
            test_services.py
        integration/
            test_api.py
    docs/
        index.md
    pyproject.toml
    uv.lock
    .gitignore
    .env.example
    Makefile
    Dockerfile
    README.md

Конфигурация с setuptools

setuptools -- стандартный инструмент сборки Python-пакетов:

# pyproject.toml with setuptools
[build-system]
requires = ["setuptools>=70.0", "setuptools-scm>=8"]
build-backend = "setuptools.backends._legacy:_Backend"

[project]
name = "mypackage"
version = "1.0.0"
description = "A well-structured Python package"
readme = "README.md"
requires-python = ">=3.12"
license = "MIT"
authors = [
    { name = "Developer", email = "[email protected]" }
]
classifiers = [
    "Programming Language :: Python :: 3.12",
    "Programming Language :: Python :: 3.13",
    "Programming Language :: Python :: 3.14",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
]

dependencies = [
    "httpx>=0.27",
    "pydantic>=2.6",
]

[project.optional-dependencies]
dev = [
    "pytest>=8.0",
    "mypy>=1.8",
    "ruff>=0.3",
    "pre-commit>=3.6",
]

[project.urls]
Homepage = "https://github.com/user/mypackage"
Documentation = "https://mypackage.readthedocs.io"
Repository = "https://github.com/user/mypackage"

[project.scripts]
myapp = "mypackage.cli:main"

[tool.setuptools.packages.find]
where = ["src"]

Конфигурация с Poetry

Poetry -- популярный инструмент управления зависимостями:

# pyproject.toml with Poetry
[tool.poetry]
name = "mypackage"
version = "1.0.0"
description = "A well-structured Python package"
authors = ["Developer <[email protected]>"]
readme = "README.md"
packages = [{ include = "mypackage", from = "src" }]

[tool.poetry.dependencies]
python = ">=3.12"
httpx = ">=0.27"
pydantic = ">=2.6"

[tool.poetry.group.dev.dependencies]
pytest = ">=8.0"
mypy = ">=1.8"
ruff = ">=0.3"

[tool.poetry.scripts]
myapp = "mypackage.cli:main"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

Конфигурация с uv

uv использует стандартный pyproject.toml и добавляет свой lock-файл:

# pyproject.toml with uv (standard format)
[project]
name = "mypackage"
version = "1.0.0"
description = "A well-structured Python package"
requires-python = ">=3.12"
dependencies = [
    "httpx>=0.27",
    "pydantic>=2.6",
]

[project.optional-dependencies]
dev = [
    "pytest>=8.0",
    "mypy>=1.8",
    "ruff>=0.3",
]

[project.scripts]
myapp = "mypackage.cli:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
# Working with uv
uv sync                  # Install dependencies
uv sync --dev            # Install with dev deps
uv run myapp             # Run the CLI tool
uv run pytest            # Run tests
uv build                 # Build the package
uv publish               # Publish to PyPI

Entry points (точки входа)

Entry points позволяют создавать CLI-команды из функций Python:

[project.scripts]
myapp = "mypackage.cli:main"
myapp-admin = "mypackage.admin:cli"
# src/mypackage/cli.py
import sys

def main() -> int:
    """Main entry point for the CLI."""
    print("Hello from myapp!")
    return 0

if __name__ == "__main__":
    sys.exit(main())

После установки пакета команда myapp будет доступна в терминале:

pip install .
myapp          # Prints "Hello from myapp!"

# Or without installation via uv
uv run myapp

Файл __main__.py

Позволяет запускать пакет через python -m:

# src/mypackage/__main__.py
"""Allow running package with: python -m mypackage"""
from mypackage.cli import main
import sys

sys.exit(main())
python -m mypackage    # Same as running myapp

Конфигурация инструментов

Все инструменты настраиваются в одном pyproject.toml:

# Ruff — linter and formatter
[tool.ruff]
target-version = "py312"
line-length = 88
src = ["src"]

[tool.ruff.lint]
select = ["E", "F", "I", "N", "UP", "B", "A", "SIM"]

[tool.ruff.lint.isort]
known-first-party = ["mypackage"]

# Mypy — type checker
[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true
warn_unused_configs = true
packages = ["mypackage"]

[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false

# Pytest
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra -q --strict-markers"
markers = [
    "slow: marks tests as slow",
    "integration: marks integration tests",
]

# Coverage
[tool.coverage.run]
source = ["mypackage"]
branch = true

[tool.coverage.report]
fail_under = 90
show_missing = true

Makefile для автоматизации

# Makefile
.PHONY: install test lint format check build

install:
	uv sync --dev

test:
	uv run pytest --cov=mypackage --cov-report=term-missing

lint:
	uv run ruff check src/ tests/
	uv run mypy src/

format:
	uv run ruff format src/ tests/
	uv run ruff check --fix src/ tests/

check: lint test

build:
	uv build

clean:
	rm -rf dist/ build/ *.egg-info
	find . -type d -name __pycache__ -exec rm -rf {} +
	find . -type f -name "*.pyc" -delete

Шаблон .gitignore

# Python
__pycache__/
*.py[cod]
*.egg-info/
dist/
build/

# Virtual environments
.venv/
venv/

# IDE
.idea/
.vscode/
*.swp

# Testing
.pytest_cache/
htmlcov/
.coverage

# Environment
.env

# OS
.DS_Store
Thumbs.db

Рекомендации по именованию

Элемент Конвенция Пример
Пакет lowercase, short mypackage
Модуль lowercase, underscores data_models.py
Класс CamelCase UserService
Функция snake_case get_user_by_id()
Константа UPPER_CASE MAX_RETRIES
Приватный _prefix _internal_helper()

Проверь себя

Какой инструмент сборки используется по умолчанию при uv init?

Что определяет секция [project.scripts] в pyproject.toml?

Где рекомендуется хранить конфигурацию mypy, ruff и pytest в современном Python-проекте?

Для чего нужен файл __main__.py в пакете?

В чем главное преимущество src layout перед flat layout?