Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → CLI на Python: typer и argparse на практике

CLI-инструменты на Python: argparse, typer и привычки хорошего интерфейса

Как писать удобные CLI на Python: argparse против typer, подкоманды и аргументы, help и коды возврата, чтение из stdin, конфигурация через env — практические конвенции.

Прямой ответ

Выбор инструмента прост: argparse — стандартная библиотека, ноль зависимостей, достаточно для скриптов и утилит, которые живут в одном репозитории; typer — поверх аннотаций типов (подпись функции = интерфейс CLI), втрое меньше кода и красивый help «бесплатно» — оптимален для инструментов, которые растут и которыми пользуются другие (тот же автор, что у FastAPI). Но важнее выбора библиотеки конвенции хорошего CLI: понятный --help, коды возврата (0 — успех, не-0 — ошибка), чтение stdin и запись в stdout (composability с grep/jq), ошибки в stderr, секреты и конфиги — из окружения, а не из аргументов командной строки (история шелла и ps их показывают).

argparse: база, которую стоит знать даже с typer

Минимальный скелет: ArgumentParser + add_argument (позиционные, --флаги, типы, значения по умолчанию, choices) + parse_args() — и уже получены help, валидация типов, --help из коробки. Продвинутые возможности, которые используются реже, чем заслуживают: подкоманды через add_subparsers (tool sync, tool check — структура как у git); взаимоисключающие группы (add_mutually_exclusive_group — «или --json, или --yaml», а не молчаливый приоритет); type= со своей функцией-парсером (валидация «на входе»); nargs/action='append' для списков; argparse читает аргументы из файла (fromfile_prefix_chars='@'). Ограничение, из-за которого появляются альтернативы: много ручной работы на нетривиальных интерфейсах и help, который надо доводить руками.

typer: аннотации как интерфейс

Тот же инструмент на typer: функция с аннотациями (def sync(env: str = Argument(..., help="Окружение"), verbose: bool = Option(False, "--verbose", "-v"))) и app() — help, типы, обязательность, дефолты выводятся из подписи (типизация как часть контракта — та же философия, что в статье про mypy). Подкоманды — @app.command(), группы — вложенные Typer(); автодополнение для bash/zsh генерируется (--install-completion); rich-вывод (цвета, таблицы, прогресс) — опциональная зависимость. Цена: зависимость в проекте и «магия», которую надо понять (Annotated-синтаксис для метаданных). Для утилиты на три флага argparse проще; для CLI с подкомандами, которым пользуются коллеги, typer окупается сразу.

Конвенции хорошего CLI (важнее библиотеки)

Свод правил, по которому утилиту «приятно использовать»: help по умолчанию при запуске без аргументов и содержательные описания каждого флага (примеры в epilog); коды возврата — успех 0, пользовательская ошибка 1–2 (и raise SystemExit/typer.Exit(code) — не traceback на каждый неверный ввод); stdout — данные, stderr — диагностика (пайп tool | jq не должен ломаться логом); поддержка stdin (- как «читать из потока») и --json-вывод для машинного потребления; --dry-run для разрушающих операций; прогресс — только в TTY (в пайпе — тишина); конфигурация — файл (~/.config/tool) и переменные окружения с явным приоритетом «флаг > env > файл > дефолт». Секреты — никогда в аргументах (видны в ps и истории шелла; подходы — в статье про env-переменные).

Упаковка и распространение утилиты

CLI становится продуктом, когда его можно установить: [project.scripts] в pyproject.toml даёт команду после pip install (механика упаковки — в статье про pyproject); альтернативы распространения — pipx install (изоляция утилиты от окружения пользователя — стандарт для глобальных CLI), single-file-дистрибуция (shiv/pex) для машин без Python-окружения. Для командных утилит — внутренний индекс или git-установка; версия — tool --version обязательна (диагностика «у всех ли одна версия» экономит часы).

Тестирование CLI

CLI тестируется как обычный код плюс слой вызова: у argparse — parse_args([...]) возвращает namespace, который проверяется напрямую (логика отделена от парсинга — правило, которое делает CLI тестируемым); у typer — CliRunner (result.exit_code, result.stdout — полный прогон команды без subprocess); сквозные проверки (реальный запуск бинарника) — пара смоук-тестов в CI. Полезный приём — фикстура «изолированная файловая система» (tmp_path) для команд, пишущих файлы: тесты не мусорят и не зависят от cwd.

Практический сценарий: утилита синхронизации с подкомандами

Спроектируем характерный CLI «tool» с двумя подкомандами — sync (тяжёт данные из API в локальную базу) и check (сверяет локальное с удалённым и печатает различия) — и посмотрим, как конвенции работают в коде. На typer: app = typer.Typer(), @app.command() для каждой подкоманды; общие опции вынесены в callback (--verbose/-v, --config с путём, дефолт ~/.config/tool.yaml, и --json для машинного вывода). Токен API читается из переменной окружения TOOL_TOKEN, а не из флага — с подсказкой в help и понятной ошибкой при отсутствии; интерактивно — typer.prompt(hide_input=True). sync поддерживает --dry-run (печатает, что изменил бы, не меняя) и читает/пишет - как stdin/stdout там, где уместно; check завершается кодом 0 при совпадении и 1 при найденных различиях — поэтому его можно встроить в cron и алертить по коду возврата, не парся вывод. Все диагностики — в stderr через typer.echo(err=True), данные — в stdout: tool check --json | jq '.diff | length' работает как часы. Тесты — через CliRunner на обеих подкомандах с замоканным HTTP-клиентом (exit_code, JSON-структура, поведение --dry-run). Установка — [project.scripts] с точкой входа, коллеги ставят через pipx, версия — tool --version из метаданных пакета. Вся утилита — триста строк, и каждая конвенция в ней экономит чьё-то время.

FAQ

click или typer? Typer построен поверх click: если нужна экосистема click-плагинов и максимальный контроль — берите click напрямую; для большинства проектов typer даёт тот же результат короче. argparse остаётся выбором «без зависимостей».

Почему мой CLI печатает traceback на неверном аргументе? Потому что исключение не поймано: оборачивайте main в try/except с человекочитаемым сообщением в stderr и ненулевым exit-кодом; traceback — для --debug-режима, а не для каждого опечатанного флага.

Как правильно читать пароль/токен? getpass/typer.prompt(hide_input=True) интерактивно, переменная окружения в скриптах, никогда — позиционный аргумент. Поддержка stdin (echo $TOKEN | tool) закрывает автоматизацию.

Стоит ли делать CLI обёрткой над HTTP API? Да, это стандартный паттерн (gh, aws): тонкая обёртка с аутентификацией из env, --json-выводом и человекочитаемым дефолтом. Такая утилита живёт дольше веб-морды и скриптуется коллегами с первого дня.

Источники: документация argparse и typer, свод правил clig.dev — ссылки в шапке.

FAQ

Как организовать конфигурацию: флаги, env или файл? Все три слоя с явным приоритетом: флаг важнее переменной окружения, та важнее файла конфигурации, дальше дефолт. Секреты — только env, часто меняемые параметры — флаги. Приоритет документируйте в --help — иначе «почему не сработал мой флаг» станет регулярным вопросом.

Нужно ли логировать каждый запуск утилиты? Для интерактивных — достаточно ошибок в stderr; для утилит в автоматизации (cron, CI) полезен файл журнала с таймстампом, аргументами без секретов и результатом — разбор «что делала утилита в три часа ночи» без журнала превращается в детектив.