コンテンツにスキップ

Pydantic でコマンドライン引数をパースする

argparseでもtypeやrequiredを指定すれば、型変換と必須チェックはこなせる。ただし引数が増えるほど、その宣言は受け取り側の型定義と二重管理になる。Pydantic のモデルへ寄せると、型変換と入力チェックの定義が 1 か所で済む。

方法は 2 つ試した。argparseと Pydantic を自分でつなぐ方法と、pydantic-settingsの CLI 機能を使う方法になる。検証環境は Python 3.14.7、pydantic 2.13.4、pydantic-settings 2.15.0 とする。

argparse と組み合わせる

モデルのフィールド名からargparseのオプションを組み立て、解析結果をモデルへ渡す。参考にした記事の方法を Pydantic v2 の API へ置き換えると、次のコードになる。

from argparse import ArgumentParser

from pydantic import BaseModel


class Arguments(BaseModel):
    bucketname: str
    filename: str
    timeout: int

    @classmethod
    def parse_args(cls):
        parser = ArgumentParser()
        for name in cls.model_fields:
            parser.add_argument(f"--{name}")
        return cls.model_validate(vars(parser.parse_args()))


args = Arguments.parse_args()
print(args)

timeoutにはargparseが文字列を渡すが、モデルを通るとintに変わる。

$ python cli.py --bucketname my-bucket --filename data.csv --timeout 60
bucketname='my-bucket' filename='data.csv' timeout=60

参考にした記事は Pydantic v1 の頃のもので、schema()とparse_obj()を呼んでいる。v2 では前者がmodel_json_schema()、後者がmodel_validate()へ名前を変えた。旧名も 2.13.4 ではPydanticDeprecatedSince20の警告つきで動くが、警告文のとおり V3.0 で削除される予定になっている。

フィールド名を取り出すだけなら、スキーマを組み立てずにmodel_fieldsを読めば足りる。

短いオプション(-bなど)を頭文字から機械的に足す実装も見かける。ただしフィールドの頭文字が重なると、引数を渡す前にArgumentParserの組み立て自体が落ちる。

argparse.ArgumentError: argument -f/--filesize: conflicting option string: -f

filenameとfilesizeのように、同じ文字で始まるフィールドは珍しくない。長いオプションだけに絞るほうが安全だ。

pydantic-settings を使う

pydantic-settingsは CLI 解析を標準で備えているため、argparseを書かずに済む。

from pydantic_settings import BaseSettings, CliApp


class Arguments(BaseSettings):
    bucketname: str
    filename: str
    timeout: int = 30


args = CliApp.run(Arguments)
print(args)

モデルに書いた既定値がそのまま効く。

$ python cli.py --bucketname my-bucket --filename data.csv
bucketname='my-bucket' filename='data.csv' timeout=30

--helpもモデルの定義から組み立てる。型と、必須か既定値かの区別まで出るので、help文字列を手で書いて保守する必要がない。

1
2
3
4
5
6
7
usage: cli.py [-h] [--bucketname str] [--filename str] [--timeout int]

options:
  -h, --help        show this help message and exit
  --bucketname str  (required)
  --filename str    (required)
  --timeout int     (default: 30)

複数のモデルを切り替えたい場合は、CliSubCommandでフィールドとして並べ、CliApp.run_subcommand()へ渡す。自分で mixin を組んで分岐する必要はない。

1
2
3
4
5
6
class Cli(BaseSettings):
    upload: CliSubCommand[Upload]
    download: CliSubCommand[Download]

    def cli_cmd(self) -> None:
        CliApp.run_subcommand(self)

UploadとDownloadはそれぞれcli_cmd()を持つBaseModelにする。実行したサブコマンドのcli_cmd()だけが呼ばれ、--helpにも{upload,download}として並ぶ。

検証エラーの見え方には注意する

エラーは 2 段階に分かれる。どこで弾かれるかによって、画面に出る内容が変わる。

パーサー段のエラーは 1 行で終わる

pydantic-settingsにはcli_exit_on_errorがあり、既定で有効になっている。受け持つのはパーサー段のエラーで、たとえば未知のオプションを渡すと usage を添えた 1 行で終わる。

usage: cli.py [-h] [--bucketname str] [--filename str] [--timeout int]
cli.py: error: unrecognized arguments: --nosuch

検証段のエラーはトレースバックになる

型の不一致や必須項目の欠落はパーサーを抜けた後の検証で見つかるため、cli_exit_on_errorの守備範囲から外れる。ここで失敗するとValidationErrorがそのまま飛び、トレースバックが利用者の画面に出る。

1
2
3
pydantic_core._pydantic_core.ValidationError: 1 validation error for Arguments
timeout
  Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='abc', input_type=str]

argparseと自分でつなぐ方法でも、検証はmodel_validate()が担うので同じトレースバックになる。他人に渡す CLI なら、どちらを選んでもValidationErrorを捕まえて自前のメッセージへ翻訳する処理が別に要る。

まとめ

観点 argparse と組み合わせる pydantic-settings
追加の依存 不要 pydantic-settingsが必要
--help 掲載例では型も既定値も出ない モデルから自動生成
既定値 add_argumentへ自分で渡す モデルの定義がそのまま効く
サブコマンド 自分で組む CliSubCommandが対応
パーサー段のエラー usage つき 1 行 usage つき 1 行
検証エラー トレースバック トレースバック

既存のargparseの実装を活かすなら Pydantic と組み合わせ、新しく書くならpydantic-settingsを選ぶとよい。ただし検証エラーの見せ方だけは、どちらを選んでも自分で決めることになる。