Импорт модуля запускает работу: отделяем CLI от функций Python

Нужно повторно использовать функцию из скрипта в тесте или другом приложении. Добавляем import, и внезапно начинается обработка файлов либо парсер требует обязательный аргумент командной строки. Функция здесь ни при чём: Python исполняет верхний уровень модуля при его первоначальном импорте. Код запуска смешался с определениями, которые хотелось использовать отдельно.

Полезная граница проходит между функцией, выполняющей предметную работу, и точкой входа, которая читает argv и выбирает код завершения. Точку входа оформим как main(argv=None). Такой аргумент позволяет тесту передать список напрямую, не изменяя sys.argv всего процесса. Обычный запуск оставит None, и argparse прочитает аргументы команды.

Точка входа main(argv)

Защитный if с __name__ нужен только в самом низу файла. Он не превращает весь модуль в функцию и не скрывает определения от импорта. При запуске файла имя равно __main__, при импорте используется имя модуля. Поэтому работа начинается по явному решению точки входа.

import argparse
from collections.abc import Sequence

def greet(name: str) -> str:
    return f"Hello, {name}"

def main(argv: Sequence[str] | None = None) -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("name")
    args = parser.parse_args(argv)
    print(greet(args.name))
    return 0

if __name__ == "__main__":
    raise SystemExit(main())

Импорт и запуск по отдельности

Сохраните код как greet.py. Команда python greet.py Ann напечатает Hello, Ann и завершится с нулевым кодом. Команда python -c "import greet" не должна печатать приветствие или требовать имя. Проверить логику можно вызовом main(["Ann"]) из теста с захватом stdout. Отдельный тест отсутствующего аргумента проверяет поведение парсера, а не самой функции greet.

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

Код завершения тоже часть интерфейса команды. Возвращаем int и передаём его в SystemExit. Если вернуть строку и передать её sys.exit, Python выведет эту строку как сообщение ошибки и завершится с ненулевым статусом. Сообщение успешной операции нужно печатать отдельно, а код статуса оставить числом.

После разделения появляется возможность подключить console script пакета к той же main или вызывать программу через python -m. При этом предметная функция не знает, из какой оболочки пришёл запрос. Проверьте обе формы использования: импорт без действий и явный вызов с результатом. Это небольшое изменение часто снимает сразу несколько причин нестабильного сбора тестов.

Следующий шаг — перенести этот приём из отдельного файла в поддерживаемое приложение. В курсе «Python для продвинутых: ООП, типизация и тестирование» структура пакетов, объектная модель, mypy и pytest соединяются в типизированном трекере задач с хранением данных и логированием. Можно отработать связь между интерфейсом, реализацией и регрессионным тестом, а затем применить её к своему проекту.

Посмотреть программу и практику курса.

Документация: официальный источник по теме.

Другие задачи по теме: Типы, тесты и поведение программы.

Продолжить на практике

  • Python для продвинутых: ООП, типизация и тестирование ↗

    Практический курс-мост от базового Python к разработке поддерживаемых приложений. Вы разберётесь со структурой пакетов, объектной моделью, современной типизацией и автоматическими тестами, а затем соедините эти инструменты в небольшом приложении с хранением данных и логированием.