Typerで作るPython CLI—argparse・clickとの違いと書き方

Typerで作るPython CLI—argparse・clickとの違いと書き方 | mohablog
目次

argparseの定型コードをTyperが消す

PythonでCLIを書くとき、最初に手が伸びるのは標準ライブラリのargparseですよね。ただ引数を1つ増やすたびにadd_argumentを呼び、型変換とヘルプ文を別々に書く手間が残ります。同じCLIを、Typerは関数の型ヒントに寄せて短く書きます。

argparseは引数定義が三度手間

名前を受け取り、回数と敬語フラグを足しただけのスクリプト。argparseだとこうなります。

import argparse

def main():
    parser = argparse.ArgumentParser(description="ユーザーに挨拶する")
    parser.add_argument("name", help="挨拶する相手の名前")
    parser.add_argument("--count", type=int, default=1, help="繰り返し回数")
    parser.add_argument("--formal", action="store_true", help="敬語にする")
    args = parser.parse_args()
    greeting = "お世話になっております" if args.formal else "やあ"
    for _ in range(args.count):
        print(f"{greeting}, {args.name}")

if __name__ == "__main__":
    main()

実行結果。

$ python argparse_ex.py 田中 --count 2 --formal
お世話になっております, 田中
お世話になっております, 田中

引数名・型・ヘルプ文がadd_argumentの中に文字列で散らばります。args.countの型はエディタからは追えません。

Typerは関数シグネチャがそのまま仕様

同じ挙動をTyperで書くと、引数定義は関数の引数そのものになります。

from typing import Annotated
import typer

def main(
    name: str,
    count: Annotated[int, typer.Option(help="繰り返し回数")] = 1,
    formal: Annotated[bool, typer.Option(help="敬語にする")] = False,
):
    """ユーザーに挨拶する"""
    greeting = "お世話になっております" if formal else "やあ"
    for _ in range(count):
        print(f"{greeting}, {name}")

if __name__ == "__main__":
    typer.run(main)
$ python typer_ex.py 田中 --count 2 --formal
お世話になっております, 田中
お世話になっております, 田中

countintformalbool。型ヒントを書いた時点で、型変換も--formal/--no-formalの生成もTyperが引き受けます。社内の集計スクリプトをargparseからTyperへ移したとき、引数定義は18行から9行に減りました。

インストールと最小構成

uvで入れて動かす

Typer 0.26.7はPython 3.10以上で動きます。uvでプロジェクトに追加します。

$ uv add typer
$ python -c "import typer; print(typer.__version__)"
0.26.7

外部のclickを別途入れる必要はありません。0.26.0でTyperがClick本体を内蔵したからです。パッケージ管理の使い分けはPythonのパッケージ管理はuvで統一すべき?pip・poetryとの違いに整理しています。

typer.run()で関数を1つだけCLIにする

コマンドが1つだけならtyper.run(関数)で十分。デコレータもクラスも要りません。引数なしで呼ぶと、Typerは自動生成した--helpを返します。

$ python typer_ex.py --help

 Usage: typer_ex.py [OPTIONS] NAME

 ユーザーに挨拶する

╭─ Arguments ──────────────────────────────────────────╮
│ *    name      TEXT  [required]                      │
╰──────────────────────────────────────────────────────╯
╭─ Options ────────────────────────────────────────────╮
│ --count                INTEGER  繰り返し回数 [default: 1]       │
│ --formal --no-formal            敬語にする [default: no-formal] │
│ --help                          Show this message and exit.    │
╰──────────────────────────────────────────────────────╯

docstringが説明文に、型ヒントが型表示に、デフォルト値が[default: 1]になりました。ヘルプ用の文字列を別に書いていません。

ArgumentとOptionはAnnotatedで書く

Typerの引数は2種類。位置引数のtyper.Argumentと、--name形式のtyper.Optionです。どちらもAnnotatedに包んで型ヒントへ添えます。

位置引数とオプションの違い

関数の引数にデフォルト値が無ければ位置引数、あればオプション。これがTyperの既定です。明示するなら次のように書きます。

from typing import Annotated
import typer

def main(
    src: Annotated[str, typer.Argument(help="入力ファイル")],
    dst: Annotated[str, typer.Option(help="出力先")] = "out.txt",
):
    print(f"{src} -> {dst}")

if __name__ == "__main__":
    typer.run(main)
$ python conv.py data.csv --dst result.txt
data.csv -> result.txt
$ python conv.py data.csv
data.csv -> out.txt

位置引数のsrcは必須。オプションのdstは省略するとout.txtに落ちます。

Annotated構文が公式の推奨

Typerは”the FastAPI of CLIs”を掲げ、タグラインは”build great CLIs. Easy to code. Based on Python type hints.”。型ヒントを軸に置く設計です。公式チュートリアルの”An alternative CLI argument declaration”では、Annotated版の使用を推奨しています。理由は”Annotated is part of standard Python”、標準の型注釈の仕組みにそのまま乗るからです。

旧構文との違い

古い書き方は、デフォルト値の位置にtyper.Option()を置きます。これだとPythonの「デフォルト値」とTyperの「オプション定義」が同じ場所に混ざります。

# 旧構文: デフォルト値の位置にOptionが入る
count: int = typer.Option(1, help="繰り返し回数")

# Annotated構文(推奨): 型とメタデータが分離する
count: Annotated[int, typer.Option(help="繰り返し回数")] = 1

どちらも動きます。ただAnnotated版ならcountの型はintのまま、デフォルトも= 1と通常のPython関数の形を保ちます。mypyやエディタの補完が素直に効きます。

サブコマンドでツールを束ねる

@app.command()で複数機能を持たせる

git addgit commitのようにコマンドを分けたいとき、typer.Typer()のインスタンスを作り、各関数に@app.command()を付けます。

from typing import Annotated
import typer

app = typer.Typer(help="ファイル管理ツール")

@app.command()
def add(name: str):
    """項目を追加する"""
    print(f"added: {name}")

@app.command()
def remove(
    name: str,
    force: Annotated[bool, typer.Option("--force", "-f")] = False,
):
    """項目を削除する"""
    print(f"removed: {name} (force={force})")

if __name__ == "__main__":
    app()
$ python tool.py --help

 Usage: tool.py [OPTIONS] COMMAND [ARGS]...

 ファイル管理ツール

╭─ Commands ───────────────────────────────────────────╮
│ add     項目を追加する                                │
│ remove  項目を削除する                                │
╰──────────────────────────────────────────────────────╯

関数名がそのままサブコマンド名に、docstringが各コマンドの説明になります。

短いフラグと真偽値オプション

typer.Option("--force", "-f")のように別名を渡すと、短いフラグも生やせます。

$ python tool.py remove old.log -f
removed: old.log (force=True)

Richの装飾とシェル補完で仕上げる

TyperはRichを依存に含み、ヘルプとエラーを罫線付きのパネルで表示します。Shellinghamも同梱で、補完対象のシェルを自動で判別します。

型エラーはRichのパネルで返る

--countに整数でない値を渡すと、変換に失敗してこう返ります。

$ python typer_ex.py 田中 --count abc
Usage: typer_ex.py [OPTIONS] NAME
Try 'typer_ex.py --help' for help.
╭─ Error ──────────────────────────────────────────────╮
│ Invalid value for '--count': 'abc' is not a valid integer. │
╰──────────────────────────────────────────────────────╯

バリデーションのコードは書いていません。型ヒントのintから、Typerが変換とエラー表示まで用意します。

補完は–install-completionで入る

サブコマンドを持つアプリには、--install-completion--show-completionが自動で付きます。前者を実行すると、現在のシェル向けの補完がインストールされます。

$ python tool.py --install-completion

bash・zsh・fish・PowerShellに対応します。コマンド名やオプションをTab補完できます。

argparse・click・Typerの使い分け

観点argparseclickTyper
追加インストール不要(標準ライブラリ)必要必要
定義スタイルadd_argument呼び出しデコレータ型ヒント
型変換type=で個別指定type=で個別指定型ヒントから自動
ヘルプ装飾素のテキスト素のテキストRichで装飾
エディタ補完効きにくい効きにくい効く

Typerは内部でClickを使うため、両者は対立しません。0.26.0からはClickを外部依存ではなく内蔵(vendoring)へ切り替えました。Typer側で依存バージョンを固定でき、Clickの更新と衝突しなくなった一方、Click向けのサードパーティプラグインやカスタム型はサポート対象外です。Clickのプラグインが必要なら、そこはClickを直接書きます。

まとめ

Typer 0.26.7で押さえた点。

  • 引数定義は関数の型ヒントに寄せる。add_argumentの文字列指定が消える
  • 引数・オプションはAnnotated構文で書く。公式が”An alternative CLI argument declaration”で推奨
  • コマンドが1つならtyper.run()、複数なら@app.command()でサブコマンド化
  • 型変換・ヘルプ生成・エラー表示・シェル補完は型ヒントから自動で付く
  • ClickはTyperに内蔵済み。別途インストールは不要、ただしClickプラグインは使えない

argparseのadd_argumentを並べる手間が気になるなら、Typerは型ヒントだけで同じCLIを組みます。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次