Организация Python проекта: main.py и модульная архитектура

Раздел: Основы Python -> Организация кода

Основной подход: модульная структура с main.py

Как создать типовую структуру Python проекта с точкой входа main.py для удобства разработки и тестирования?

Наиболее распространённая и эффективная организация кода предполагает размещение исполняемого файла main.py в корне проекта, а всю логику – в отдельном каталоге src (или app). Такой подход разделяет ответственность: main.py служит только точкой входа, а остальные модули содержат бизнес-логику, утилиты и тесты.

Пример базовой структуры


project/
├── main.py
├── src/
│   ├── __init__.py
│   ├── core.py
│   └── utils.py
├── tests/
│   ├── __init__.py
│   └── test_core.py
├── requirements.txt
├── README.md
└── .gitignore

Python сделать модуль (создание модуля в python)

Шаги создания:

  1. Создайте корневую директорию project/.
  2. Внутри создайте main.py – он будет запускать приложение.
  3. Создайте папку src/ с файлом __init__.py (пустым или с инициализацией пакета).
  4. В src/ разместите модули: core.py (основная логика) и utils.py (вспомогательные функции).
  5. Папка tests/ для тестов с собственным __init__.py.
  6. Добавьте requirements.txt и README.md для описания проекта.

Пример кода main.py


import sys
from src.core import run_app
from src.utils import setup_logging

def main():
    setup_logging()
    config = {"mode": "production"}
    result = run_app(config)
    print(f"Приложение завершено с результатом: {result}")

if __name__ == "__main__":
    main()

Python project main py (структура проекта python с main.py)

Пояснения

  • Импорты из src работают благодаря наличию __init__.py и нахождению корня проекта в sys.path (по умолчанию при запуске main.py корень добавляется автоматически).
  • Блок if __name__ == "__main__": гарантирует, что код выполняется только при прямом запуске файла, а не при импорте.
  • Функция main() инкапсулирует логику запуска, что упрощает тестирование.

Типичная ошибка: ImportError при попытке импортировать модуль из src. Причина: запуск main.py из другой директории или отсутствие файла __init__.py. Решение: запускать скрипт из корня проекта (python main.py) и убедиться, что все папки-пакеты содержат __init__.py.


Альтернативные варианты организации

Вариант 1. Всё в одном файле main.py (для мелких скриптов)

Как сделать простой однофайловый скрипт без разделения на модули?

Подходит для скриптов, состоящих из 50–100 строк. Логика, функции и точка входа находятся в одном файле.

Пример


#!/usr/bin/env python3
# main.py (единственный файл)

import sys
import requests

def fetch_data(url):
    response = requests.get(url)
    response.raise_for_status()
    return response.json()

def process_data(data):
    return [item["name"] for item in data if "name" in item]

def main():
    url = sys.argv[1] if len(sys.argv) > 1 else "https://api.example.com/data"
    data = fetch_data(url)
    names = process_data(data)
    for name in names:
        print(name)

if __name__ == "__main__":
    main()

Проблемы: при увеличении размера кода (более 200 строк) такой файл становится трудно читать и поддерживать. Невозможно переиспользовать части кода в других проектах без копирования. Тестирование отдельных функций затруднено, так как все зависит от глобального состояния.

Вариант 2. main.py + модули в той же папке (для небольших проектов)

Как организовать код, если проект вырос до нескольких файлов, но пока не требует глубокой иерархии?

Файлы раскладываются в корне проекта, main.py вызывает функции из соседних .py-файлов.

Структура


project/
├── main.py
├── helpers.py
├── config.py
├── requirements.txt

main.py:


from config import load_config
from helpers import greet

def main():
    cfg = load_config()
    msg = greet(cfg.get("user", "Гость"))
    print(msg)

if __name__ == "__main__":
    main()

Недостаток: по мере роста количество файлов в корне увеличивается, что усложняет навигацию. Отсутствие чёткой структуры приводит к путанице между модулями разного назначения. Также могут возникать конфликты имён с библиотеками.

Вариант 3. Использование пакетов с __init__.py (стандартный для крупных проектов)

Как структурировать большой проект с подмодулями и избежать проблем с импортами?

Включает в себя иерархию пакетов: src/core/, src/utils/ и т.д. Каждый подкаталог является пакетом благодаря __init__.py.

Пример структуры


project/
├── main.py
├── src/
│   ├── __init__.py
│   ├── core/
│   │   ├── __init__.py
│   │   ├── engine.py
│   │   └── models.py
│   └── utils/
│       ├── __init__.py
│       ├── io_helpers.py
│       └── logging_setup.py
├── tests/
│   ├── __init__.py
│   └── test_engine.py
├── config/
│   ├── default.yaml
│   └── production.yaml

main.py:


from src.core.engine import start_engine
from src.utils.logging_setup import configure_logging

def main():
    configure_logging("config/default.yaml")
    start_engine()

if __name__ == "__main__":
    main()

Распространённая ошибка: относительные импорты внутри пакетов (например, from ..core import something) могут вызвать ImportError при запуске main.py, если не использовать -m. Решение: всегда запускать main.py как python -m src.core.engine или с абсолютными импортами, начиная с корня проекта.

Вариант 4. Установка проекта как пакета с entry_points (для CLI-приложений)

Как сделать проект устанавливаемым и вызываемым из командной строки через одну команду?

Добавляется setup.py (или pyproject.toml) с указанием точек входа. Файл main.py может отсутствовать, его заменяет функция, указанная в console_scripts.

Пример setup.py


from setuptools import setup, find_packages

setup(
    name="myapp",
    version="0.1.0",
    packages=find_packages(),
    entry_points={
        "console_scripts": [
            "myapp = src.cli:main",
        ],
    },
    install_requires=[
        "requests>=2.25",
    ],
)

В src/cli.py определяется функция main(), которая обрабатывает аргументы командной строки. После установки (pip install .) можно запускать приложение командой myapp из любого места.

Проблема: при разработке необходимо переустанавливать пакет после каждого изменения. Решение – использовать pip install -e . (режим редактирования).

Расширенные примеры и пояснения

Пример 1. Полноценная структура с обработкой аргументов, логированием и тестами

Рассмотрим проект простого веб-скрапера, организованный по модульному принципу с пакетами.

Пример

scraper/
├── main.py
├── src/
│   ├── __init__.py
│   ├── fetcher.py
│   ├── parser.py
│   └── output.py
├── tests/
│   ├── __init__.py
│   ├── test_fetcher.py
│   └── test_parser.py
├── config/
│   └── settings.ini
├── requirements.txt
└── README.md

main.py – точка входа:

Пример

#!/usr/bin/env python3
"""Главный модуль для запуска скрапера."""
import argparse
import sys
from src.fetcher import fetch_page
from src.parser import extract_links
from src.output import save_to_file

def parse_args():
    parser = argparse.ArgumentParser(description="Веб-скрапер для извлечения ссылок")
    parser.add_argument("url", help="URL страницы для анализа")
    parser.add_argument("-o", "--output", default="links.txt", help="Файл для сохранения ссылок")
    parser.add_argument("--timeout", type=int, default=10, help="Таймаут запроса в секундах")
    return parser.parse_args()

def main():
    args = parse_args()
    print(f"Загрузка {args.url}...")
    html = fetch_page(args.url, timeout=args.timeout)
    links = extract_links(html, base_url=args.url)
    save_to_file(links, args.output)
    print(f"Найдено {len(links)} ссылок. Результат сохранён в {args.output}.")

if __name__ == "__main__":
    main()

src/fetcher.py:

Пример

import requests
from requests.exceptions import RequestException

def fetch_page(url, timeout=10):
    """Получает HTML-содержимое страницы."""
    try:
        response = requests.get(url, timeout=timeout)
        response.raise_for_status()
        return response.text
    except RequestException as e:
        print(f"Ошибка при запросе {url}: {e}", file=sys.stderr)
        raise

src/parser.py:

Пример

from urllib.parse import urljoin
from bs4 import BeautifulSoup

def extract_links(html, base_url):
    """Извлекает все ссылки (теги <a>) из HTML."""
    soup = BeautifulSoup(html, "html.parser")
    links = []
    for a in soup.find_all("a", href=True):
        href = a["href"]
        absolute_url = urljoin(base_url, href)
        links.append(absolute_url)
    return links

src/output.py:

Пример

def save_to_file(links, filename):
    """Сохраняет список ссылок в текстовый файл."""
    with open(filename, "w", encoding="utf-8") as f:
        for link in links:
            f.write(link + "\n")

tests/test_parser.py:

Пример

import pytest
from src.parser import extract_links

HTML_SAMPLE = '''
<html>
<body>
    <a href="/page1">Page 1</a>
    <a href="https://example.com/page2">Page 2</a>
    <a href="#section">Section</a>
</body>
</html>
'''

def test_extract_links():
    base_url = "https://example.com"
    links = extract_links(HTML_SAMPLE, base_url)
    assert len(links) == 3
    assert links[0] == "https://example.com/page1"
    assert links[1] == "https://example.com/page2"
    assert links[2] == "https://example.com/#section" # Якорь присоединяется к base_url

Результат выполнения теста (при запуске pytest tests/):

============================= test session starts ==============================
collected 1 item

tests/test_parser.py .                                                  [100%]

============================== 1 passed in 0.12s ===============================

Пример 2. Использование относительных импортов внутри пакета

При разработке глубоко вложенных пакетов иногда удобно обращаться к соседним модулям через относительные импорты. Однако такая практика требует запуска через -m. Рассмотрим структуру:

Пример

project/
├── main.py
├── pkg/
│   ├── __init__.py
│   ├── subpkg/
│   │   ├── __init__.py
│   │   ├── a.py
│   │   └── b.py

pkg/subpkg/a.py:

Пример

def func_a():
    return "A"

pkg/subpkg/b.py (использует относительный импорт):

Пример

from .a import func_a

def func_b():
    return func_a() + "B"

main.py:

Пример

from pkg.subpkg.b import func_b

print(func_b())

Запуск:

Пример

python -m main

Результат:

AB

Если запустить python main.py (без -m), то возникнет ошибка ImportError: attempted relative import with no known parent package. Поэтому при использовании относительных импортов необходимо всегда запускать скрипт как модуль: python -m project.main (если проект установлен как пакет) или python -m main при нахождении в корне.

Пример 3. Динамическая загрузка конфигурации из YAML

В больших проектах полезно выносить параметры в конфигурационные файлы. Покажем, как загрузить настройки в main.py.

Пример

# config/default.yaml
debug: false
port: 8080
database:
  host: localhost
  port: 5432
  name: mydb
Пример

# src/config_loader.py
import yaml
from pathlib import Path

def load_config(path="config/default.yaml"):
    config_path = Path(__file__).parent.parent / path
    with open(config_path, "r", encoding="utf-8") as f:
        config = yaml.safe_load(f)
    return config
Пример

# main.py
from src.config_loader import load_config

def main():
    config = load_config()
    print(f"Порт: {config['port']}")
    print(f"База данных: {config['database']['name']}")

if __name__ == "__main__":
    main()

Результат выполнения:

Порт: 8080
База данных: mydb

Примечание: для работы с YAML требуется библиотека PyYAML (pip install PyYAML). В данном примере путь к конфигу формируется относительно расположения скрипта, что позволяет не зависеть от рабочей директории.

Структура проекта Python с main.py - comments

En
Python project main py (python)