nikkie-ftnextの日記

イベントレポートや読書メモを発信

python-build-standaloneのAstral社移管に思うこと

はじめに

Astral、あんたまた管理するリポジトリ増やして!1 nikkieです。

Pythonコミュニティにとってよいニュースではあると思うのですが、(意見の偏りがあると自覚しつつも)私の目から見たAstral社にはモヤッとするポイントもあり、1本書くことにしました。

目次

python-build-standaloneがAstral社に移管されました

python-build-standaloneは、Gregory Szorc氏によるプロジェクトです。
現在注目されているuvはPython処理系を管理できます2が、これを可能にしているのがpython-build-standalone!
uvだけでなく、HatchやRyeのPython管理機能も支えています。
Thank you very much, Gregory!

Gregory氏によるメンテナンスが難しくなったそうで3、Astral社は2024年4月からリリースに加わっていたとのこと。

We've shepherded every release since April, (ç•¥)

12月に突然移管したわけではなく、移行期間を設けての動きだったようです。
コミュニティにおけるpython-build-standaloneの存在感を考えると、Astralが手を挙げて動いたという点には感謝です。

上記の移管発表ブログには4つのゴールが掲げられていました(The future of standalone Python distributions)

  1. Keep the project up-to-date with Python releases(Pythonのリリースに合わせてpython-build-standaloneを最新に保つ)
  2. Upstream changes to the CPython build system(上流のCPythonのビルドシステムに変更を反映する)
  3. Remove some of the project's existing limitations(python-build-standaloneに存在する制約を取り払う)
  4. Improve the project's build and release process(python-build-standaloneのビルドとリリースプロセスの改善)

python-build-standaloneは私も触って、Python開発環境構築のあまりの簡単さに衝撃を受けました(Gregory氏に技術力で殴られた...)。
メンテナンスが引き継がれることはPythonコミュニティにとって好ましいことだと思っています。

モヤッとポイント:管理するリポジトリ全部、要望に応え続けられる?

Astral社に感謝の思いはあるのですが、それが100%というわけではなく、私は半分くらいモヤッとしています。

Astral社はRuff、uv(、Rye)と注目度の高いツールをいくつも開発しています。
ちゃんと全部お世話し続けられるのかが、私から見ると不安です。
(私が重箱の隅をつついてるのかもしれませんが、)これまでのAstral社の行動からお世話し続けると確信できないんですよ

具体的には、uvやRuffでコミュニティの要望が高い機能が放置されているように私の目には映っています。
要望が高いにも関わらず、Astral社は実現をリードしていない(ように見える)。
これが私のモヤッとです。
現状お世話しきれていないのに、新しい管理対象を増やしたんだ〜と思っちゃうんですよね。

いくつか例を出しましょう。

たとえばuvにはタスクランナー機能が要望されています。
私もuvで管理するプロジェクトにuvxを並べたMakefileを毎回作っているので、これはめちゃほしいです。

ところが
https://github.com/astral-sh/uv/issues/5903#issuecomment-2277047047

Yeah we plan to support something like this! We haven't spent time on the design yet.

(意訳)このようなものをサポートすることを計画しています。まだ設計に時間を費やしていません

このコメントは2024年8月。
このIssueはたまに見るくらいなのですが、2025年年始時点でOpenのままなんですね。
多くのuvユーザが待っていると思うんですが、半年間でも設計には足りなかったのでしょうか?(優秀なエンジニア集まってると思うんで着手したら速いはず。ということは着手していない??)
コミュニティの議論が盛り上がっているのが救いかなと思います。

(※uvのリリースを細かく追ってないので、タスクランナー的な機能がリリース済みでしたら教えてください)

もう1つ、Ruffにもあります。

Ruffが置き換えようとしているFlake8にはプラグインがあります。
コミュニティがFlake8プラグインを自由に開発できるため、Flake8は支持されてきた面があると言えるでしょう。

さてRuffですが、Flake8より高速という一点張りなんですよ。
Ruffがプラグインをサポートしたら私にはもうFlake8にこだわる理由はほぼないように思われる4のですが、Issueは2年間進展がありません。

uvのタスクランナーも、Ruffのプラグイン機構も、コミュニティが望んでいるのにAstral社はなぜ何もしないんでしょうね?(なにか都合でも悪いんでしょうか?)
python-build-standaloneが同じ状態にならないと私は言い切ることができません。

終わりに

python-build-standaloneのAstral社移管について思うことを書きました。

  • Gregoryさんありがとう。そしてAstral社も2024年の大部分をかけてpython-build-standaloneを引き継いでくれてありがとう
  • でもuvã‚„Ruffがコミュニティの要望が高い機能を放置しているように見えるんですが、python-build-standaloneでも繰り返さないと言えますか?(行動で示してほしいです)

この記事を書いた手前、私も緊張感を持って注視していきつつ、小さくともプルリクを送れるようになるべく、1日1エントリなど使っていきたいなあという気持ちです


  1. 念頭にあるのはこちら
  2. uv pythonコマンド https://docs.astral.sh/uv/reference/cli/#uv-python
  3. Gregory氏視点はこちら Gregory Szorc's Digital Home | Transferring Python Build Standalone Stewardship to Astral
  4. 似た意見 https://github.com/astral-sh/ruff/issues/283#issuecomment-2329103310

Python環境構築DASH村。python-build-standaloneのおかげで、curlかwgetだけあれば余裕です✌️

はじめに

「「「「けど」」」」 nikkieです。

知的好奇心を満たすための手作り、再び開催です!1

※これは学習用途なので、実際にはHatchやuvなどpython-build-standaloneを中で使っているツールを使ってください!

目次

python-build-standalone

創始者はGregory Szorc氏。
この12月にAstral社へ移管されました。

移管を発表したAstral社のブログにpython-build-standaloneがどんなものか書いてあります(「What's a "standalone" Python distribution?」)。

CPythonはstandaloneではない。
すなわち、ビルドしたシステムと強く結びつく

  • Linuxã‚„macOSでは、いくつかのシステムパスがバイナリにハードコードされる
  • いくつかのシステムライブラリに動的リンクする

なのでソースをダウンロードしてマシンでビルドするというアプローチが採られてきた(例:pyenv)わけです。
しかし、このアプローチには欠点もあります

  1. 事前にビルドされたバイナリのダウンロードと比べて、遅い
    • (IMO:ソースダウンロード + バイナリビルドの工程を考えたら時間はかかりますよね)
  2. ビルドツールチェーンの依存(例:gcc)を導入することになるが、失敗しうる
    • (IMO:経験がめっちゃあります...)

python-build-standaloneはこれらの問題に対処した、事前ビルド済みのバイナリです。
依存の静的リンクと、相対パスで動くようCPythonのビルドシステムへのパッチによって対処したそうです

Specifically, python-build-standalone solves these problems by (1) statically linking Python against its dependencies; and (2) patching the CPython build system to operate on relative, rather than absolute paths.

uv, Hatch, Ryeなど昨今はPython処理系も含めて管理するパッケージマネージャが出現していますが、これらの裏にはpython-build-standaloneがあるんです!
ということで、python-build-standaloneでPython環境を作ってみます。

python-build-standaloneでPythonをインストール!

参考にした記事はこちら

合わせてドキュメントも確認しました。

python-build-standaloneはGitHubのReleasesにバイナリが置かれます。
https://github.com/astral-sh/python-build-standalone/releases
ぶっちゃけここからファイルを指定してダウンロードしてくるだけです。

URLの命名規則ですが、「asset_url_prefix + ファイル名」です。

asset_url_prefix

% curl -L -s https://raw.githubusercontent.com/astral-sh/python-build-standalone/latest-release/latest-release.json | jq -r '.asset_url_prefix'
https://github.com/indygreg/python-build-standalone/releases/download/20241219

ファイル名は(今回はM1 Macなので)「cpython-3.12.8+20241219-aarch64-apple-darwin-install_only.tar.gz」

  • aarch64-apple-darwin
  • install-only2
    • Casual users will likely want to use the install_only archive, as most users do not need the build artifacts present in the full archive.

% wget 'https://github.com/indygreg/python-build-standalone/releases/download/20241219/cpython-3.12.8+20241219-aarch64-apple-darwin-install_only.tar.gz' -O distribution.tar.gz
% # または
% curl -L 'https://github.com/indygreg/python-build-standalone/releases/download/20241219/cpython-3.12.8+20241219-aarch64-apple-darwin-install_only.tar.gz' -o distribution.tar.gz

チェックサムを確認しておきましょう

% diff -s <(curl -s -L 'https://github.com/indygreg/python-build-standalone/releases/download/20241219/cpython-3.12.8+20241219-aarch64-apple-darwin-install_only.tar.gz.sha256') <(shasum -a 256 distribution.tar.gz | cut -d' ' -f1)
Files /dev/fd/11 and /dev/fd/12 are identical
% echo $?
0

解凍!

% tar xf distribution.tar.gz

pythonディレクトリができており

.
├── distribution.tar.gz
└── python/
    ├── bin/
    ├── include/
    ├── lib/
    └── share/
% python/bin/python3.12 -V
Python 3.12.8

処理系、入手!🙌 🙌 🙌

なにこれ! めっちゃ簡単になってますね。
このPython処理系をPATHが通ったところに配置すればよさそうに思われます(Hatchやuvをパクりたい)

python-build-standaloneで入手したPythonのpipでパッケージインストール

pipが同梱されている3のでパッケージのインストールもできます!

まずは仮想環境を作ります
(今回は後回しにしましたが、python/bin/python3.12がPATHの通った場所に置かれた後を想定しています)

% python/bin/python3.12 -m venv .venv --upgrade-deps
% .venv/bin/python -V
Python 3.12.8

(仮想環境をactivateしたらpython -Vだけで済みますが、ちょっとしたこだわりから今回はこちらでいきます)

% .venv/bin/python -m pip install kojo-fan-art
% .venv/bin/kojo-day kokoro
{"kokoro": "Tuesday"}

すんなりいきましたが、Zennの記事からつまづきポイントだと思っていました。
.venv/bin/python -m siteを見ます。

この状態なのですが、pip installしたライブラリは(USER_SITEではなく)仮想環境に置かれました。
仮想環境はsys.pathに含まれるので、kojo-dayコマンドが使えます。

この挙動を説明するドキュメントはまだ見つけられていないのですが、当初はZenn記事にならってPYTHONUSERBASEを指定していました。
https://docs.python.org/ja/3/using/cmdline.html#envvar-PYTHONUSERBASE
PYTHONUSERBASE=$PWD/.venv .venv/bin/python -m siteでUSER_SITEがsys.pathに含まれることを確認できます

% PYTHONUSERBASE=$PWD/.venv .venv/bin/python -m pip install kojo-fan-art  # 結果的に、PYTHONUSERBASEの指定は不要

(クリアにできていないところもありますが)python-build-standaloneでPython処理系を取得し、仮想環境を作ってそこにインストールできました!

終わりに

python-build-standaloneでPython環境を作る素振りでした

  • 命名規則に従ってGitHubのReleasesからダウンロードして解凍するだけ!
    • 宿題:今回のやり方だと毎回ディレクトリにPython処理系が置かれるので、マシンに1個にしたい(Hatchã‚„uvを見てみよう)
  • pipが同梱されており、仮想環境を作ってパッケージのインストールまでできた!
    • 宿題:USER_SITEが仮想環境ではないのに、pip installで仮想環境に入るのなんでだろう?

今後はcurlかwgetさえあれば、私はPython環境が作れると思います💪
2020年代のPython環境構築はこんなに便利になっているんですな〜(先人、特にGregoryさんに大感謝🫶🫶🫶)


  1. 前回のDASH村
  2. full archiveならpgo+ltoかなと思っています。「These should be the fastest distributions since they have the most build-time optimizations.」
  3. 意図はこちら「The intent of the pre-installed software is to facilitate end-user package installation without having to first bootstrap a packaging tool via an insecure installation technique (such as curl | sh patterns).」

pydantic-settingsで環境変数からもオプション引数を指定できるCLIを作る 〜サブコマンド篇〜

はじめに

幕が上がる 瞬間が好き♪ nikkieです。

pydantic-settingsでCLIの素振りの続き、今回はサブコマンドまわりを触ります。

目次

pydantic-settingsでCLIが作れる!

パースライブラリPydanticを設定にも適用するpydantic-settings。
なんと設定だけでなく、CLIアプリケーションも作れちゃいます!

pydantic-settingsで作るCLIのよさは、オプション引数を環境変数からも指定できること。
標準ライブラリのargparseでこれを実現するのはいささか骨が折れそうなのですが、pydantic-settingsではデフォルトでサポートされているんです!
argparseすきすきの民の私ですが、これは乗り換えを決意するのに十分すぎる理由でした(なので素振りをしています)

前回の素振りより、オプション引数・位置引数の扱いが分かっています。
pydantic_settings.BaseSettingsを継承したクラスにて

  • オプション引数:string: str = Field(validation_alias=AliasChoices("s", "string"))
    • -sã‚„--stringで指定できる
    • 環境変数Sã‚„STRINGでも指定できる(素晴らしすぎる!!👏)
    • デフォルト値を持たせることも可能(Field("デフォルト値", validation_alias=...))
  • 位置引数:string: CliPositionalArg[str]
    • 位置引数は環境変数からは指定できません(位置で指定しましょう)
    • 位置引数にデフォルト値は持たせられない模様1

サブコマンド

Rustのclapで素振りしたのと同様なCLIを今回作りました2。

% uv run --quiet cli.py position エミリー 5
エミリーエミリーエミリーエミリーエミリー
% OPTION_S=エミリー uv run --quiet cli.py option --num 5
エミリーエミリーエミリーエミリーエミリー

動作環境

  • uv 0.4.27 (Homebrew 2024-10-25)
  • Python 3.13.0
  • pydantic-settings 2.7.0
    • Pydantic 2.10.4

ドキュメント「Subcommands and Positional Arguments」より
https://docs.pydantic.dev/latest/concepts/pydantic_settings/#subcommands-and-positional-arguments

class Settings(BaseSettings):
    position: CliSubCommand[Position]
    option: CliSubCommand[Option]
  • サブコマンドの実体は
    • PydanticのBaseModelを継承したクラス
    • または、pydantic.dataclasses.dataclassでデコレートしたクラス
    • (IMO:サブコマンドにはBaseSettingsは継承しないんですね!)
  • BaseSettingsを継承したクラス(コマンドの大元)では、CliApp.get_subcommand()を使う
  • 設定のとき3と同様に、環境変数は「BaseSettings継承クラスの属性名」+「区切り文字」+「サブコマンドのエイリアス」でした
    • 区切り文字はenv_nested_delimiterでアンダースコアを指定
    • 列挙すると、OPTION_S・OPTION_STRING・OPTION_N・OPTION_NUM
  • (前回記事より)CliApp.run(Settings)でSettingsインスタンスのcli_cmd()メソッドを呼び出してます

サブコマンドもできた〜!🙌

なお、Noteに制約の記述が

it does not allow for multiple subparsers with each subparser having its own set of subcommands.

直面したわけではないのでピンときてはいないのですが、argparseみたいにサブコマンド・サブサブコマンドとネストさせられないってことですかね?
BaseSettingsを継承したクラスをCliSubCommandアノテーションできないようですし。
argparseでは使うことがあったのでちょっと痛いかもしれないですが、サブサブコマンドが必要になったときに考えようかと思います(サブコマンドだけに合わせて、コマンドの設計を見直すことになりそう)

終わりに

pydantic-settingsで作るCLIで、サブコマンドの作り方を完全に理解しました!
clapの素振りでも書きましたが、位置引数・オプション引数・サブコマンドを押さえたので、argparseで書いているCLIの8割くらいは、pydantic-settingsでも書けそうです(※馬鹿の山感)。

Pydanticですから書いてて型がバチバチ当たりますし、環境変数からオプションを指定できるのは広く使われるCLIは備えている印象のある便利機能で、pydantic-settingsは推したいライブラリになりました。
君が天才!


  1. argparse(やRustのclap)ではできていました。位置引数のデフォルト値をそれほど使っていない私としては、あまり痛くはないです
  2. uvがinline script metadataを読んでいるという出力(「Reading inline script metadata」)をしなくする--quietを見つけました。https://docs.astral.sh/uv/reference/cli/#uv-run
  3. 設定の素振りの様子:pydantic-settings素振りの記:ネストした設定 〜BaseModel継承クラスが全ての属性でデフォルト値を持つならば、インスタンス化してBaseSettings側のデフォルト値とする〜 - nikkie-ftnextの日記

え! PydanticのdataclassデコレータとBaseModel継承は同じ、じゃないんですか!?

はじめに

明けましておめでとうニョロ〜🐍 nikkieです。

私はPydanticのdataclassesをBaseModelと同じ(つまり交換可能)だと誤解していました...

目次

きっかけ

tokibitoさんのブログを読んでいたところ

pydanticのドキュメントには、BaseModelを継承するのとdataclassを使うのでは、機能的な差があるとも書かれている

な、なんだって〜!?
Pydanticのdataclassデコレータは『ロバストPython』を読みましたが、読み落としたかな?

ドキュメントを見に行きます。

docs.pydantic.dev

Noteより

Keep in mind that Pydantic dataclasses are not a replacement for Pydantic models. They provide a similar functionality to stdlib dataclasses with the addition of Pydantic validation.

There are cases where subclassing using Pydantic models is the better choice.

意訳
PydanticのdataclassesはPydanticモデルと交換できないことに注意してください。それら(※Pydanticのdataclassesと理解)は標準ライブラリのdataclassesにPydanticのバリデーションを加えたのに似た機能を提供します。
Pydanticモデルをサブクラスにするほうがより良い選択となるケースがあります。

詳しくはこちらのissueが案内されます1

結論:dataclassデコレータとBaseModel継承には違いがあります

https://github.com/pydantic/pydantic/issues/710#issuecomment-1242014214

  • 可変な(mutable)デフォルト値の指定方法
  • インスタンス化における余剰な(extra)フィールド

(このIssueには他の違いの報告も続きます)

1️⃣可変なデフォルト値の指定方法

https://github.com/pydantic/pydantic/issues/710 の内容です

pydantic.BaseModelを継承する場合、空のリストをデフォルト値として指定できます。

class X(BaseModel):
    list_: list[int] = []

一方、pydantic.dataclasses.dataclassでデコレートする場合、空のリストをデフォルト値として指定すると(標準ライブラリのdataclasses)がValueErrorを送出します。

@dataclass
class Y:
    list_: list[int] = []

ValueError: mutable default for field list_ is not allowed: use default_factory

エラーにあるようにdefault_factoryを使う必要があります。
default_factoryの指定は以下のどちらでもよいとありました2。

@dataclass
class Y:
    list_: list[int] = field(default_factory=list)
    # list_: list[int] = Field(default_factory=list)  # こちらでも同じ

いずれの場合も、デフォルト値に指定した空のリストはインスタンス間で共有されません。

x1 = X()
x2 = X()
print(f"{x1.list_=}, {x2.list_=}")
x1.list_.append(1)
print(f"{x1.list_=}, {x2.list_=}")

# y1, y2も同様
x1.list_=[], x2.list_=[]
x1.list_=[1], x2.list_=[]

y1.list_=[], y2.list_=[]
y1.list_=[1], y2.list_=[]

2️⃣インスタンス化における余剰なフィールド

BaseModelを継承する場合、model_configで余剰フィールドを許可したり拒否したりできます。
https://docs.pydantic.dev/latest/api/config/#pydantic.config.ConfigDict.extra

Whether to ignore, allow, or forbid extra attributes during model initialization. Defaults to 'ignore'.

しかし、@dataclassデコレータの場合は、model_configで指定しても余剰フィールドは無視されます。

class X(BaseModel):
    model_config = ConfigDict(extra="allow")

    list_: list[int] = []


@dataclass
class Y:
    model_config = ConfigDict(extra="forbid")

    list_: list[int] = field(default_factory=list)


x = X(foo="bar")
y = Y(foo="bar")
print(f"{x=}, {y=}")
x=X(list_=[], foo='bar'), y=Y(list_=[])

動作検証スクリプト

inline script metadataを使ってhatchで動かしました

  • Hatch, version 1.14.0
  • Python 3.12.0
  • Pydantic 2.10.4

終わりに

今回はPydanticのdataclassデコレータとBaseModel継承の違い2点を見ました。

  • 可変な値をデフォルト値として指定する場合
    • BaseModel継承ではそのままでOK
    • dataclassデコレータでは、標準ライブラリのfieldまたはPydanticのFieldでdefault_factoryに指定する
  • インスタンス化における余剰なフィールド
    • BaseModel継承ではmodel_configでextraを設定可能
    • dataclassデコレータではmodel_configを書いても無視される

2つは機能的に等しいもの(それゆえ交換可能)と誤解していました。
これからはBaseModelを継承していきます!

ここまで調べると「じゃあなんで(下位互換に思われる)dataclassデコレータがPydanticにあるんだろう」と考えちゃいますよね。
作者曰く標準ライブラリのdataclassデコレータを置き換えるという目的とのことです(BaseModel継承と等価にしようとしたわけではないと理解)
https://github.com/pydantic/pydantic/issues/710#issuecomment-530751709

We should make it clear that pydantic.dataclasses.dataclass is (mostly) a drop in replacement for dataclasses.dataclass with validation, not a replacement for pydantic.BaseModel.


  1. このissueに対応するpull requestでNoteの内容が追加されたようでした。
  2. 「You can use both the Pydantic's Field() and the stdlib's field() functions」 ref: https://docs.pydantic.dev/latest/concepts/dataclasses/

nikkie v2024.12 リリースのお知らせ

はじめに

2024年、大変お世話になりましたドラ〜! nikkieだゴン〜!1
皆さま、よいお年をお迎えドラゴン〜

12月のふりかえり記事です。
11月はこちら

目次

1日1エントリ継続中

11月で2年経過し、3年目。
「ナムコのななー」な765日を超えました。
読んでいただきありがとうございます!

1年近くの時を経てホッテントリした記事が! わーい🙌

技術まわり

FastAPI

12月はFastAPIまわりのインプット・アウトプットが多い月でした。

11月に夢中になっていたGitHub Copilot Extensions Python実装で得た知識にも助けられました。

FastAPIがきっかけで素振りしたライブラリ

  • SQLModel
  • SQLAlchemy
  • uvicorn
  • pydantic-settings

2月のDjangoCongressJP(「Async Future」)のネタを考えてもいいかも〜

SpeechRecognitionメンテナ

わたしがんばりました〜(えらい!)
12月は2回リリースしています

テストコードがないコードベースに、皆さんからテストを書かずに場当たり的なハックによる機能追加PRがしばしば届きます。
「マージしたら(瞬間は)楽になる」という誘惑に抗い、中長期の便益を考えてテストを少しずつ整えながらここまできました2

RESPXを知れたのは大きいです

技術同人 欠席続き

それぞれ欠席の理由は違うのですが、連続しちゃったのでなにか対策を考えようかな。

遠い締切3より目先の興味という自分の性質をどう飼いならすかってことじゃないかと思っています。

サブカルまわり

エンジニアニメ

アドベントカレンダーしました!

SHIROBAKOからの学びを発表もしました!

アドカレのおかげで、アニメ関連の活動が多い月だったかもしれませんね。

アイマスエキスポ

怪文書を頒布しました!
最高でした...(別途記事を書きたい気持ち)

その後ミリシタでエミリーちゃん上位報酬イベント(All Alone)もあって、存分に楽しんでます!

アオのハコ

...狂わされております

雛様...(深いため息)
千夏先輩...(これまた深いため息)

終わりに

nikkie v2024.12がリリースされました。

  • FastAPIや関連するライブラリ(SQLModel、pydantic-settingsなど)のインプット・アウトプットが多かった月
  • RESPXを知ったことで、SpeechRecognitionではテスト書いてリファクタリングを回してます!
  • アイマスエキスポやアオのハコ、エンジニアニメとサブカルまわりも充実してました

そして、次のバージョン(v2025.01)に向けての旅が続くのです!


  1. 2023年大晦日は語尾が「ぴょん」だったんですよね。干支フィーチャー!
  2. 「テストを書こう」という想いを登壇ネタにしてもいいかもしれないですね。世界中からテストのないPRが届くので、世界に伝えたい
  3. 遠くともこれが自分にとって重要と思えたら、とかかなあ

pydantic-settingsで環境変数からも指定できるオプション引数を持つCLIを作る

はじめに

#ミリアニブレイバーン異文化すぎる交流😂😂😂 nikkieです。

年の終わりに激震が走りました。
私はargparseすきすきの民なのですが、突然の別れが訪れてしまったかもしれません。
え、pydantic-settings、CLIアプリケーションも作れるの!?

目次

pydantic-settings

Pydanticはパースライブラリ。
実行時の型のバリデーションとして機能し、とても便利です(FastAPIや、LLMに出力させたJSONのパース)

そんなPydanticの機能を設定に適用できるのが、pydantic-settings。
先日素振りしました。

さてpydantic-settingsのドキュメントを見ていたところ、見つけてしまったのです!
「Command Line Support」という項目を。

pydantic-settingsによる「Command Line Support」

https://docs.pydantic.dev/latest/concepts/pydantic_settings/#command-line-support

過去にこんなCLIアプリケーションを作りました。

$ python repeat.py -s シオン -n 3
シオンシオンシオン

repeat.pyをpydantic-settingsで再現します。
PEP 723(inline script metadata)を使いました。

  • uv 0.4.27 (Homebrew 2024-10-25)1
  • Python 3.13.0
  • pydantic-settings 2.7.0
    • Pydantic 2.10.4

% uv run repeat.py --string エミリー --num 5
エミリーエミリーエミリーエミリーエミリー
% uv run repeat.py -s エミリー -n 5  # オプションのエイリアス
エミリーエミリーエミリーエミリーエミリー
% uv run repeat.py -s エミリー  # numの方はデフォルト値を設定しています
エミリーエミリー

特筆したいのは、設定のときと同様、環境変数からも指定できることです。

% STRING=エミリー uv run repeat.py
エミリーエミリー
% STRING=エミリー N=5 uv run repeat.py  # 環境変数もエイリアスをサポート!
エミリーエミリーエミリーエミリーエミリー

これはargparseでは簡単にはできないと思われます(もっと強い人からしたら「hahaha 一瞬だよ〜」かもですが)。
こんなに簡単にできることを見せられたら2、pydantic-settingsは最初の選択肢としてかなり有力に映ります。

なお、ヘルプメッセージも生えていました。

% uv run repeat.py --help
usage: repeat.py [-h] [-s str] [-n int]

options:
  -h, --help        show this help message and exit
  -s, --string str  (required)
  -n, --num int     (default: 2)

「Creating CLI Applications」

ドキュメントには、「CLIアプリケーションを作る」という項目がありました。
https://docs.pydantic.dev/latest/concepts/pydantic_settings/#creating-cli-applications

CliApp.run()を使います。
https://docs.pydantic.dev/latest/api/pydantic_settings/#pydantic_settings.CliApp

差分はこちら

from pydantic import AliasChoices, Field
-from pydantic_settings import BaseSettings, SettingsConfigDict
+from pydantic_settings import BaseSettings, CliApp

class Settings(BaseSettings):
-    model_config = SettingsConfigDict(cli_parse_args=True)

-Settings().cli_cmd()
+CliApp.run(Settings)
  • CliApp.run()は渡したSettingsã‚’
    • デフォルトではsys.argv[1:]からインスタンス化(cli_args引数)
    • さらにデフォルトではcli_cmd()メソッドを呼び出します(cli_cmd_method_name引数)
  • cli_parse_args=True(model_config)が不要になっています
% uv run repeat.py -s エミリー
エミリーエミリー
% STRING=エミリー uv run repeat.py -n 5  # 環境変数も引き続きサポート!
エミリーエミリーエミリーエミリーエミリー

なお、CliAppはBaseSettingsを継承したクラスだけでなく、BaseModelを継承したクラスやpydantic.dataclasses.dataclassでデコレートしたクラスもCLIアプリケーションにできるとのことでした。すっげー!
https://github.com/pydantic/pydantic-settings/blob/v2.7.0/pydantic_settings/main.py#L443-L444

位置引数は?

「Subcommands and Positional Arguments」より
https://docs.pydantic.dev/latest/concepts/pydantic_settings/#subcommands-and-positional-arguments
位置引数はCliPositionalArgを使います。

差分

from pydantic import AliasChoices, Field
-from pydantic_settings import BaseSettings, CliApp
+from pydantic_settings import BaseSettings, CliApp, CliPositionalArg

class Settings(BaseSettings):
-     string: str = Field(validation_alias=AliasChoices("s", "string"))
+     string: CliPositionalArg[str]
% uv run repeat.py エミリー
エミリーエミリー
% uv run repeat.py エミリー -n 5
エミリーエミリーエミリーエミリーエミリー

STRING=hoge uv run repeat.py エミリーは環境変数が無視されますね。
(STRING=hoge uv run repeat.pyはSTRINGの指定がないというエラーです)
位置引数扱いになってる!

終わりに

pydantic-settingsを使うと、環境変数からも指定できるオプション引数を持つCLIが手軽に作れます!

  • BaseSettingsを継承したクラスをCliApp.run()に渡すだけ
    • コマンドはcli_cmd()メソッドに実装
  • オプション引数のエイリアスはField(validation_alias=AliasChoices("s", "string"))のように指定
  • 位置引数はCliPositionalArgを使う

argparseと同じ水準で使える3ようにサブコマンドについて素振りを残していますが、単純なCLIアプリケーションであればまずはpydantic-settingsを試してみたいと思いました。
ライブラリのユーザがほとんど何もしてないのに、環境変数から設定できるのはヤバいですよ!


  1. ちょっと古め。最新は0.5系です
  2. 自力で実装する一例(過去のソースコードリーディングより)
  3. RustのCLAPの素振り(現在の到達点)

メンテナ記 | pipx run(やuvx)でパッケージングも楽々だ〜!🫶

はじめに

「アルストロメリア、なんて」 うおおおおおおおお😭 nikkieです。

SpeechRecognition メンテナ活動の中で知ったことです。
(なお、3.13.0をリリースしました🎉)

目次

これまでのpipx run

PyPI(など)で配布されるPythonパッケージにはimportせずにコマンドラインだけで使うツールも数多く存在します。
そんなツールを、開発者による仮想環境の管理不要で最新バージョンを使えるようにしてくれるのが、pipx run!

このタダ乗りはヤバいです。
今すぐ全Python使いに導入してほしい!(なお後述のuvxでもよいです)

恩恵を享受しているSphinxの例

pipx runの代案としてはuvx1。
これはuv tool runの略です。

docs.astral.sh

(uvがいいと思っている方は、uvxもぜひ使いこなしてください〜)

GitHubではpipxが使いやすい

あくまで執筆時点ですが、GitHub ActionsやGitHub Codespacesではpipxがプレインストールされています。
(今後uvが入ることも十分ありえます)

GitHub Actionsの例

これはpipx runを使わない手はありません!

パッケージング手順をpipx runでアップデート

SpeechRecognitionメンテナ活動では、PyPIへのリリースはGitHub Codespacesで行っています2

実行するツールは2つ

  1. build
  2. twine

今回PyPIのbuildを偶然確認したところ、pipx runの例に気づきました!
https://pypi.org/project/build/1.2.2.post1/

私はpipx runによる仮想環境管理不要な開発の味を占めているので、これを使わない手はありません。
合わせてtwineもpipx runで実行するようにしました。

今後の作業を楽にするためにMakefileに追加!
https://github.com/Uberi/speech_recognition/blob/3.13.0/Makefile#L8-L10

distribute:
    @pipx run build
    @pipx run twine check dist/*

どこかでpipx run twine uploadも追加しようっと(トークンの扱いが絡むのでサッとできず)

終わりに

パッケージングやPyPIへのリリースのために実行するbuildやtwineは、pipx runで仮想環境管理不要で楽々できることを知りました。
pipx runやuvxがもたらしてくれたパラダイムや世界は本当に素晴らしいですし、全Python使いに恩恵を享受してほしいなと思います

P.S. twine checkで怒られた

Sphinxすきすき民としては涙目...

https://packaging.python.org/ja/latest/guides/making-a-pypi-friendly-readme/#validating-restructuredtext-markup

Sphinx 拡張はこの場所では使用を許されず


  1. uvにおけるtool https://docs.astral.sh/uv/concepts/tools/
  2. 自分のリポジトリのように自動化できるといいのですが、そのためにはowner Uberiさんに権限をいただく必要があるので、コミュニケーションを後回しにしちゃってるんですよね