CLI на Python: click или typer — что выбрать и как написать
Разбираем два способа сделать командную утилиту на Python: click с декораторами и typer на аннотациях типов. Что выигрываете и чем платите за каждый.

Вам нужно превратить скрипт в утилиту с флагами, подкомандами и понятным --help. Голый sys.argv и argparse из стандартной библиотеки работают, но парсинг аргументов вручную быстро превращается в кашу из if-ов, а argparse многословен и не умеет из коробки цветной вывод, автодополнение и вложенные группы команд. Две библиотеки закрывают эту задачу: click и typer. Ниже — чем они отличаются на практике, когда какую брать и где каждая ломается.
Версии, для которых написано: click 8.1.x, typer 0.12.x, Python 3.10+. Typer построен поверх click, поэтому многое из поведения click проявляется и в typer.
Разница в одном предложении
Click описывает интерфейс декораторами (@click.option, @click.argument), typer выводит его из аннотаций типов функции. Один и тот же результат — но подход к объявлению параметров прямо противоположный.
Минимальная команда на click
import click
@click.command()
@click.argument("name")
@click.option("--count", default=1, help="Сколько раз повторить")
def hello(name, count):
"""Печатает приветствие name раз count."""
for _ in range(count):
click.echo(f"Привет, {name}")
if __name__ == "__main__":
hello()
Запуск: python app.py Аня --count 3. Тип --count click вывел из default=1 — целое число, и сам отвергнет --count abc с ошибкой.
То же самое на typer
import typer
app = typer.Typer()
@app.command()
def hello(name: str, count: int = 1):
"""Печатает приветствие name раз count."""
for _ in range(count):
typer.echo(f"Привет, {name}")
if __name__ == "__main__":
app()
Здесь count: int = 1 задаёт и тип, и значение по умолчанию, и то, что это опция с флагом --count. Позиционный name: str без значения по умолчанию становится обязательным аргументом. Меньше строк, но вся конфигурация теперь спрятана в сигнатуре.
Где проявляется разница
Явность против краткости
В click каждый параметр — отдельный декоратор, где видно всё: имя флага, help, тип, значение по умолчанию, обязательность. Это многословно, но при чтении кода не нужно держать в голове правила вывода. В typer правила вывода надо знать: например, чтобы задать help для параметра, аннотацию приходится усложнять через Annotated.
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def hello(
name: str,
count: Annotated[int, typer.Option(help="Сколько раз повторить")] = 1,
):
for _ in range(count):
typer.echo(f"Привет, {name}")
Как только параметрам нужны help, валидаторы, диапазоны и метавары, сигнатура typer раздувается Annotated-обёртками и по объёму догоняет click, теряя изначальную краткость.
Подкоманды
Утилита вроде tool db migrate и tool db reset требует групп. В click группа — это @click.group(), к которой команды подключаются декоратором. В typer вы создаёте отдельный Typer() и монтируете его через app.add_typer.
# click
import click
@click.group()
def cli():
pass
@cli.command()
def migrate():
click.echo("Применяю миграции")
@cli.command()
def reset():
click.echo("Сбрасываю базу")
# typer
import typer
app = typer.Typer()
db = typer.Typer()
app.add_typer(db, name="db")
@db.command()
def migrate():
typer.echo("Применяю миграции")
@db.command()
def reset():
typer.echo("Сбрасываю базу")
Валидация и типы
Click предлагает готовые типы параметров: click.Path(exists=True) проверит, что файл есть, click.Choice(["json", "csv"]) ограничит значения, click.IntRange(1, 100) задаст диапазон. Typer использует свои эквиваленты и хорошо дружит с enum.Enum для перечислений — enum автоматически превращается в набор допустимых значений.
Таблица сравнения
| Критерий | click | typer |
|---|---|---|
| Способ объявления | Декораторы над функцией | Аннотации типов в сигнатуре |
| Многословность простого случая | Выше | Ниже |
| Сложный случай с help и валидаторами | Читается предсказуемо | Раздувается через Annotated |
| Основа | Самостоятельная | Надстройка над click |
| Автодополнение shell | Есть | Есть, включается проще |
| Зависимости | Одна | Тянет click как зависимость |
| Зрелость и стабильность API | Очень зрелая | Моложе, API ещё меняется |
Typer не замена click, а другой фасад над ним. Если упрётесь в предел typer, вы всё равно окажетесь в терминах click — поэтому знать click полезно в обоих случаях.
Когда что выбирать
- Берите typer, если утилита небольшая, параметры простые, а команда уже пишет типизированный код и любит аннотации. Экономия строк здесь реальная.
- Берите click, если много параметров с валидацией, кастомными типами, сложными группами и колбэками, либо если хотите минимум магии и максимум явности при код-ревью.
- Уже есть argparse-скрипт и нет времени — иногда дешевле оставить argparse. Переписывать рабочую утилиту ради красоты декораторов смысла нет.
Что ломается на практике
- Забытый вызов группы. В typer с подкомандами точка входа —
app(), а не сама функция. Вызовете функцию напрямую — получитеTypeErrorпро отсутствующие аргументы. - Дефис против подчёркивания. Параметр
max_sizeв коде в CLI станет--max-size. Внутри функции обращаетесь поmax_size, в терминале вводите--max-size. Путаница на этом — классика. - print вместо echo. Обычный
printработает, ноclick.echo/typer.echoкорректно обрабатывают кодировки и перенаправление вывода. На Windows и в пайпах разница заметна. - Тесты. Не запускайте утилиту через
subprocessв тестах. Click даётCliRunnerдля вызова команд в процессе и проверки кода возврата и вывода — typer предоставляет свою обёртку поверх него. - Коды возврата. Чтобы завершить с ненулевым кодом, поднимайте
typer.Exit(code=1)илиclick.exceptions.Exit, а неsys.exitпосреди логики — иначе теряете единообразную обработку.
Упаковка в устанавливаемую команду
Чтобы вместо python app.py набирать просто mytool, объявите точку входа в pyproject.toml. Для проекта с секцией [project]:
[project.scripts]
mytool = "mypackage.cli:app"
Здесь mypackage.cli — модуль, app — объект Typer (или функция группы click). После pip install . в окружении появится команда mytool. Точный синтаксис секции сверьте с документацией вашей системы сборки — у setuptools, hatchling и poetry поля называются по-разному.
Что делать дальше
Соберите скелет на выбранной библиотеке, добавьте одну команду с обязательным аргументом и одной опцией, проверьте вывод --help. Дальше подключите автодополнение shell (--install-completion в typer или генерацию скрипта в click) и напишите первый тест через CliRunner — это отсечёт регрессии при добавлении новых команд. Валидация путей и работа с переменными окружения — отдельная тема, здесь мы её не разворачивали.
Аналитик звонков: Разбор записи звонка по этапам сделки: где сорвалось, что упущено, три исправляемые рекомендации с цитатами.
Спросить за 90 ₽Источники: Click documentation, Typer documentation
Частые вопросы
У меня другая версия — typer 0.9 или click 7. Код заработает?
Что выбрать новичку, который раньше писал только argparse?
Не сработало: команда не находится в терминале после pip install
Как вернуть ненулевой код выхода при ошибке?
Можно ли смешивать click и typer в одном проекте?
Как тестировать CLI без запуска подпроцесса?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.