$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогTyper для продакшн: подкатанды, конфиги из файлов и “умные” обработчики ошибок

Typer для продакшн: подкатанды, конфиги из файлов и “умные” обработчики ошибок

$ sudo teach IT
·23 июля 2026 г.·9 мин·42
Typer для продакшн: подкатанды, конфиги из файлов и “умные” обработчики ошибок

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

Содержание
Архитектура CLI: команды как интерфейс, а не как набор функцийПодкатанды в Typer: группируем команды и уменьшаем когнитивную нагрузкуКогда подкатанды действительно нужныБазовый шаблон с подкатандамиПрактический нюанс: разделяйте Typer-приложения, а не только функцииКонфиги из файлов: где хранить настройки и как их правильно комбинироватьВыбор формата конфигурацииМодель конфигурации и типизацияЗагрузка конфигурации из файлаСлой объединения: файл + опции CLIПодключение конфигурации к командамПодводные камниExit-коды как контракт: предсказуемость для автоматизацииРекомендуемая базовая матрицаExit в Typer: лучше не бросать “случайные” исключения“Умные” обработчики ошибок: единый формат, подсказки и корректные кодыШаг 1: определим доменные исключенияШаг 2: преобразуем существующие ошибки в доменныеШаг 3: единый обработчик для TyperЧто это дает“Умность”: где подсказки, а где — точная причинаВалидация параметров и “мягкие” проверки до исполнения логикиПример: пользовательский параметр, который требует контекст

Typer — один из тех Python-инструментов, который приятно начинаешь “просто чтобы сделать CLI”, а затем внезапно обнаруживаешь, что он упирается в те же вопросы, что и в любом продакшн-проекте: как организовать команды, где хранить конфиги, как отвечать пользователю при ошибках и как корректно завершать процесс (exit code), чтобы CLI можно было надежно встроить в скрипты CI/CD.

В этой статье разберем практическую архитектуру CLI на Typer, ориентированную на продакшн-использование: подкатанды, загрузку настроек из файлов, единый слой валидации и “умные” обработчики ошибок. Приведем рабочие примеры кода и разберем типичные грабли, из‑за которых CLI выглядит «работающим», но не является удобным для пользователей и предсказуемым для автоматизации.


Архитектура CLI: команды как интерфейс, а не как набор функций

В учебных примерах Typer часто показывают структуру:

  • функция = команда
  • параметры = аргументы/опции
  • декоратор @app.command() = привязка

Но в продакшене “команда” — это часть интерфейса, который должен быть:

  1. Стабильным (не рушиться при добавлении новых фич).
  2. Организованным (пользователь понимает, где что).
  3. Понятным в ошибках (сообщения локализованы, корректные exit-коды, подсказки).
  4. Интегрируемым (скрипты и пайплайны предсказуемы).

Ключевые элементы продакшн-архитектуры:

  • Подкатанды (subcommands) — группировка функциональности.
  • Загрузка конфигурации — разделение “кода” и “настроек”.
  • Единая модель ошибок — единый способ формировать сообщения и выходной код.
  • Согласованная валидация — лучше провалиться раньше, чем получить странное поведение.

Подкатанды в Typer: группируем команды и уменьшаем когнитивную нагрузку

Когда подкатанды действительно нужны

Почти всегда стоит переходить к подкатандам, если у вас больше 5–7 команд, или если команды относятся к разным доменам: например, db migrate, db seed, db status, deploy staging, deploy prod, auth login.

Подкатанды улучшают UX за счет:

  • уменьшения длины “плоского” списка команд,
  • возможности показывать справку по доменам,
  • упрощения будущего расширения.

Базовый шаблон с подкатандами

Ниже — рабочий каркас, где верхнеуровневое приложение держит подкатанды. В реальном проекте так удобнее разнести команды по модулям.

code
# app/main.py
import typer

from app.commands.db import db_app
from app.commands.deploy import deploy_app

app = typer.Typer(
    help="CLI для управления проектом: база данных и деплой."
)

app.add_typer(db_app, name="db", help="Операции с БД")
app.add_typer(deploy_app, name="deploy", help="Деплой приложений")

if __name__ == "__main__":
    app()
code
# app/commands/db.py
import typer

db_app = typer.Typer(help="Команды для миграций и статуса БД")

@db_app.command("migrate")
def migrate(
    dry_run: bool = typer.Option(False, "--dry-run", help="Без применения изменений"),
):
    """
    Применить миграции.
    """
    if dry_run:
        typer.echo("Dry run: миграции не будут применены.")
        raise typer.Exit(code=0)

    typer.echo("Миграции применены (демо).")
code
# app/commands/deploy.py
import typer

deploy_app = typer.Typer(help="Команды для деплоя")

@deploy_app.command("staging")
def staging(version: str = typer.Option(..., "--version")):
    """
    Деплой в staging.
    """
    typer.echo(f"Деплой staging: версия {version} (демо).")

Практический нюанс: разделяйте Typer-приложения, а не только функции

Не пытайтесь делать “псевдоподкатанды” через --mode или длинные опции. Subcommand — это не украшение. Это способ:

  • держать команды “рядом” с доменной логикой,
  • проще документировать,
  • легче тестировать: вы проверяете конкретные входы/выходы каждой группы.

Конфиги из файлов: где хранить настройки и как их правильно комбинировать

Вопрос №1 для продакшна: каков источник настроек?

Обычно схема такая:

  1. Конфигурация по умолчанию (встроенная или в файле).
  2. Файл конфигурации, указанный пользователем.
  3. Опции командной строки, которые переопределяют то, что в файле.

Эта “лестница приоритетов” должна быть предсказуемой. Пользователь должен понимать: если я задал --timeout 30, он реально переопределит timeout=10 из файла?

Выбор формата конфигурации

На практике для CLI чаще всего выбирают:

  • YAML (читаемый, удобен для вложенных структур),
  • JSON (строгий, легко парсится),
  • иногда TOML (понятный формат, есть нативные библиотеки).

Покажем YAML-подход. Если вам удобнее JSON — принцип тот же.

Модель конфигурации и типизация

Даже если ваш CLI маленький, лучше явно определить структуру конфигурации. Можно использовать dataclasses или pydantic. Для демонстрации — dataclass.

code
# app/config.py
from dataclasses import dataclass

@dataclass(frozen=True)
class AppConfig:
    timeout_seconds: int = 10
    environment: str = "dev"
    log_level: str = "INFO"

Загрузка конфигурации из файла

code
# app/config_loader.py
from pathlib import Path
import yaml

from app.config import AppConfig

class ConfigError(Exception):
    pass

def load_config(path: Path | None) -> AppConfig:
    if path is None:
        return AppConfig()

    if not path.exists():
        raise ConfigError(f"Файл конфигурации не найден: {path}")

    try:
        raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
    except yaml.YAMLError as e:
        raise ConfigError(f"Некорректный YAML в файле {path}: {e}") from e

    # Простая маппинг-логика (в продакшене обычно делают валидацию/нормализацию)
    return AppConfig(
        timeout_seconds=int(raw.get("timeout_seconds", 10)),
        environment=str(raw.get("environment", "dev")),
        log_level=str(raw.get("log_level", "INFO")),
    )

Слой объединения: файл + опции CLI

Typer удобен, но он не построит вашу “лестницу приоритетов” за вас. Лучше сделать это явно.

code
# app/settings.py
from dataclasses import replace
from pathlib import Path

from app.config import AppConfig
from app.config_loader import load_config

def resolve_config(
    config_file: Path | None,
    timeout_seconds: int | None,
    environment: str | None,
    log_level: str | None,
) -> AppConfig:
    cfg = load_config(config_file)

    if timeout_seconds is not None:
        cfg = replace(cfg, timeout_seconds=timeout_seconds)
    if environment is not None:
        cfg = replace(cfg, environment=environment)
    if log_level is not None:
        cfg = replace(cfg, log_level=log_level)

    return cfg

Подключение конфигурации к командам

Вы не обязаны тянуть конфиг во все команды вручную. Но в продакшене удобно сделать базовый общий “контекст” через зависимость (через callback или через фабрику). Typer поддерживает контекст Context.

Пример: добавим опцию --config в каждую группу команд, а внутри резолвим конфиг.

code
# app/commands/db.py
import typer
from pathlib import Path

from app.config import AppConfig
from app.settings import resolve_config
from app.config_loader import ConfigError

db_app = typer.Typer(help="Команды для миграций и статуса БД")

def config_options():
    return {
        "config_file": typer.Option(
            None,
            "--config",
            "-c",
            help="Путь к файлу конфигурации YAML",
            exists=True,
            file_okay=True,
            dir_okay=False,
            readable=True,
        ),
        "timeout_seconds": typer.Option(
            None, "--timeout", help="Переопределить таймаут (в секундах)"
        ),
        "environment": typer.Option(
            None, "--env", help="Переопределить окружение (например, dev/staging/prod)"
        ),
        "log_level": typer.Option(
            None, "--log-level", help="Переопределить уровень логов"
        ),
    }

@db_app.command("status")
def status(
    config_file: Path = typer.Option(None, "--config", "-c"),
    timeout_seconds: int | None = typer.Option(None, "--timeout"),
    environment: str | None = typer.Option(None, "--env"),
    log_level: str | None = typer.Option(None, "--log-level"),
):
    """
    Показать статус БД.
    """
    try:
        cfg = resolve_config(
            config_file=config_file,
            timeout_seconds=timeout_seconds,
            environment=environment,
            log_level=log_level,
        )
    except ConfigError as e:
        # Выше мы вынесли модель ошибок — позже сделаем единый обработчик
        raise typer.Exit(code=2) from e

    typer.echo(f"Environment: {cfg.environment}")
    typer.echo(f"Timeout: {cfg.timeout_seconds}s")
    typer.echo(f"Log level: {cfg.log_level}")
    typer.echo("DB status: OK (демо).")

Подводные камни

  • Приоритет источников: всегда документируйте, что именно перекрывает. Иначе пользователь будет “подкручивать” опции, но не понимать, что они игнорируются из‑за неверной схемы.
  • Валидация файла: не полагайтесь на то, что ключи будут корректного типа. Ошибки преобразования должны быть превращены в понятные сообщения.
  • exists=True в Typer: удобно, но его поведение может вызывать ранние ошибки парсинга до ваших обработчиков. Если вы хотите управлять стилем сообщений ошибок — иногда лучше убрать exists=True и проверять внутри.

Exit-коды как контракт: предсказуемость для автоматизации

Продакшн-CLI почти всегда используется в сценариях:

  • Makefile
  • GitHub Actions / GitLab CI
  • скрипты деплоя
  • пайплайны миграций

Для них важен контракт: какой exit code соответствует какой проблеме.

Рекомендуемая базовая матрица

Пример “разумной” схемы (можете подстроить под домен):

  • 0 — успешное выполнение
  • 1 — общая ошибка выполнения (неизвестная/не классифицированная)
  • 2 — ошибка в конфигурации (файл отсутствует/невалиден/не соответствует схеме)
  • 3 — ошибка пользователя (неверные параметры/логика запроса)
  • 4 — сетевые/внешние зависимости (таймауты, отказ в доступе, сервис недоступен)

Главное правило: одинаковая причина → одинаковый код.

Exit в Typer: лучше не бросать “случайные” исключения

Typer использует исключения, и по умолчанию некоторые ошибки могут приводить к exit code 1. Для контроля вам нужно:

  • либо явно выбрасывать typer.Exit(code=...),
  • либо перехватывать исключения и управлять кодом в одном месте.

Мы сделаем единый обработчик.


“Умные” обработчики ошибок: единый формат, подсказки и корректные коды

Пользовательский опыт в CLI часто ломается не кодом ошибки, а тем, как она представлена:

  • “Traceback (most recent call last)” вместо нормального объяснения.
  • Без указания, какой параметр некорректен.
  • Непонятно, что делать дальше.
  • Ошибка конфигурации выглядит как Python-исключение, а не как проблема в данных.

Мы сделаем обработку на двух уровнях:

  1. Классификация доменных ошибок (валидатор, конфиг, внешний сервис).
  2. Единый формат вывода и exit code.

Шаг 1: определим доменные исключения

code
# app/errors.py
class CLIError(Exception):
    """
    Базовая ошибка CLI.
    """
    exit_code: int = 1
    user_message: str = "Ошибка выполнения"

    def __init__(self, message: str | None = None):
        super().__init__(message or self.user_message)

class ConfigCLIError(CLIError):
    exit_code = 2
    user_message = "Ошибка конфигурации"

class ParameterCLIError(CLIError):
    exit_code = 3
    user_message = "Неверные параметры"

class ExternalCLIError(CLIError):
    exit_code = 4
    user_message = "Ошибка внешнего сервиса"

Шаг 2: преобразуем существующие ошибки в доменные

Допустим, ваш load_config сейчас кидает ConfigError. Преобразуем.

code
# app/config_loader.py (фрагмент)
from pathlib import Path
import yaml

from app.config import AppConfig
from app.errors import ConfigCLIError

def load_config(path: Path | None) -> AppConfig:
    if path is None:
        return AppConfig()

    if not path.exists():
        raise ConfigCLIError(f"Файл конфигурации не найден: {path}")

    try:
        raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
    except yaml.YAMLError as e:
        raise ConfigCLIError(f"Некорректный YAML в {path}: {e}") from e

    try:
        return AppConfig(
            timeout_seconds=int(raw.get("timeout_seconds", 10)),
            environment=str(raw.get("environment", "dev")),
            log_level=str(raw.get("log_level", "INFO")),
        )
    except (TypeError, ValueError) as e:
        raise ConfigCLIError(f"Ошибка типов в конфигурации {path}: {e}") from e

Шаг 3: единый обработчик для Typer

В Typer можно перехватывать исключения на уровне приложения. Проще всего — использовать @app.callback() или просто обернуть app() в try/except в __main__. Однако правильнее поддерживать единый формат везде.

Вариант с wrapping в main.py:

code
# app/main.py
import typer

from app.commands.db import db_app
from app.commands.deploy import deploy_app
from app.errors import CLIError

app = typer.Typer(
    help="CLI для управления проектом: БД и деплой."
)

app.add_typer(db_app, name="db", help="Операции с БД")
app.add_typer(deploy_app, name="deploy", help="Деплой приложений")

def main():
    try:
        app()
    except CLIError as e:
        # Печатаем на stderr, чтобы пользователи и логгеры могли различать
        typer.secho(f"Error: {e}", err=True, fg=typer.colors.RED)
        raise typer.Exit(code=e.exit_code) from e

if __name__ == "__main__":
    main()

Что это дает

  • Никакого traceback для “ожидаемых” ошибок.
  • Error: ... в одном стиле.
  • Разные причины → разные exit codes.
  • Сторонние ошибки (которые не наследуются от CLIError) по-прежнему покажут traceback (или их можно тоже классифицировать).

“Умность”: где подсказки, а где — точная причина

Хорошее сообщение — это не “что-то пошло не так”, а минимум:

  • что случилось,
  • где (параметр/файл),
  • что делать (корректная команда/пример).

Например, если файл конфигурации отсутствует, можно добавить “пример как указать путь”:

code
raise ConfigCLIError(
    f"Файл конфигурации не найден: {path}. "
    f"Укажите корректный путь через --config."
)

Если параметр --version должен соответствовать regex, сообщите: “ожидается формат X.Y.Z”.


Валидация параметров и “мягкие” проверки до исполнения логики

Typer дает базовую валидацию типов (int, float, bool). Но “умная” валидация — это ваша ответственность.

Пример: пользовательский параметр, который требует контекст

Допустим, команда db migrate принимает --target (версия миграции). Логика может требовать, чтобы --dry-run и --target сочетались с ограничениями: например, при --dry-run нельзя делать --apply.

Если это правило появится только внутри “длинного” кода, пользователь потеряет время и получит странную ошибку.

Лучше проверить в начале:

code
# app/commands/db.py (фрагмент)
import typer

from app.errors import ParameterCLIError

@db_app.command("migrate")
def migrate(
    target: str = typer.Option(None, "--target", help="До какой миграции откатить/применить"),
    dry_run: bool = typer.Option(False, "--dry-run"),
):
    if dry_run and target is None:
        # пример: в вашем домене это правило может быть другим
        raise ParameterCLIError("--dry-run требует

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

Typer PRO

Курс по теме

Typer PRO

С нуля до уверенной работы с CLI-проектами на Python. Подходит для начинающих и тех, кто уже пишет на Python. Разберитесь с Typer раз и навсегда — и перестаньте копировать команды вслепую.

Открыть курс

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу
Простыми словами о защищенном соединении

Простыми словами о защищенном соединении

Базовый курс для обычных людей: что такое защищённое соединение, как оно устроено, что оно умеет и чего не умеет, как выбрать сервис и настроить его на своих устройствах.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Сервис, который не падает - как построить безопасную зашифрованную сеть

Сервис, который не падает - как построить безопасную зашифрованную сеть

Практический курс по безопасности сети: как собрать собственный сервис защищённого доступа - от пустого сервера до подписки с автовыбором - и, что важнее, как потом не уронить его своими руками.

Перейти к курсу

Другие статьи

Пишем CLI для разработчика на Python: команды, конфиги и корректные exit codes по стандарту проекта
пишем

Пишем CLI для разработчика на Python: команды, конфиги и корректные exit codes по стандарту проекта

Построим удобный интерфейс командной утилиты: структура подкоманд, чтение конфигов, валидация входных параметров и единая логика ошибок с понятными exit code. Сфокусируемся на UX для тех, кто запускает CLI в CI и в скриптах.

15 сентября 2026 г.
1030
Docker Compose для реальных проектов: конфиги, сети, volume и воспроизводимые окружения
docker

Docker Compose для реальных проектов: конфиги, сети, volume и воспроизводимые окружения

Научимся поднимать набор сервисов одним файлом, правильно разделять окружения и делать так, чтобы «у меня работает» исчезло.

20 июля 2026 г.
730
Создание Telegram бота в 2026 легко и просто! Полный курсы!
создание

Создание Telegram бота в 2026 легко и просто! Полный курсы!

19 июня 2026 г.
1431
Как выбрать, куда идти в IT: веб, боты, данные — без хаоса
выбрать

Как выбрать, куда идти в IT: веб, боты, данные — без хаоса

Разберём самые понятные направления для новичка (веб, боты, аналитика/данные) и чем они отличаются по шагам обучения. В конце вы получите простой план, как проверить, что вам подходит, и какой стартовый путь выбрать.

5 октября 2026 г.
580
Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть
путь

Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть

Разберём структуру репозитория, как упаковать проект в понятную историю (проблема → решение → запуск → метрики качества) и какие артефакты повышают доверие: скринкасты, архитектурная схема, чек-лист продакшн-готовности.

17 сентября 2026 г.
1090
Как понять, какой язык программирования учить первым: простой тест и выбор
понять

Как понять, какой язык программирования учить первым: простой тест и выбор

Помогу новичку выбрать первый язык без стресса: объясню варианты по целям (веб, боты, анализ данных), что учить в первую очередь и как не потратить месяцы впустую. В конце — краткий план старта на ближайшие недели.

2 октября 2026 г.
630

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

Архитектура CLI: команды как интерфейс, а не как набор функцийПодкатанды в Typer: группируем команды и уменьшаем когнитивную нагрузкуКогда подкатанды действительно нужныБазовый шаблон с подкатандамиПрактический нюанс: разделяйте Typer-приложения, а не только функцииКонфиги из файлов: где хранить настройки и как их правильно комбинироватьВыбор формата конфигурацииМодель конфигурации и типизацияЗагрузка конфигурации из файлаСлой объединения: файл + опции CLIПодключение конфигурации к командамПодводные камниExit-коды как контракт: предсказуемость для автоматизацииРекомендуемая базовая матрицаExit в Typer: лучше не бросать “случайные” исключения“Умные” обработчики ошибок: единый формат, подсказки и корректные кодыШаг 1: определим доменные исключенияШаг 2: преобразуем существующие ошибки в доменныеШаг 3: единый обработчик для TyperЧто это дает“Умность”: где подсказки, а где — точная причинаВалидация параметров и “мягкие” проверки до исполнения логикиПример: пользовательский параметр, который требует контекст