はじめに
以前に Goを推奨する記事 の中で、「ツール用言語としてPythonの体験が依存性管理等で良くない」ということを書いたのですが、 uv (GitHub) というRust製のPythonパッケージマネージャーツールで体験がかなり改善しそうだったので、この記事で試していきたいと思います。
(この記事のソースコードは https://gitlab.com/ksaitou/2025-03-17_tryout_uv で入手可能です)
uvのインストール
インストール ページを見る限り、通常のパッケージマネージャに対応しているようです。
# macOS
$ brew install uv
# Windows
$ winget install --id=astral-sh.uv -e
$ scoop install main/uv
# Cargo (Rustのパッケージマネージャ)
$ cargo install --git https://github.com/astral-sh/uv uv
シングルファイルPythonの実行にuvを使う
uvがインストールされた状態で以下の小さなツールのスクリプトを実行してみます。
# /// script
# requires-python = ">=3.13"
# dependencies = [
# "datetime",
# "requests",
# ]
# ///
import requests
import datetime
report_source = "https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json"
if __name__ == "__main__":
response = requests.get(report_source)
data = response.json()
for area in data[0]['timeSeries'][0]['areas']:
if area['area']['name'] == '東京地方':
weather_tokyo = area['weathers']
break
today = datetime.datetime.now()
tomorrow = today + datetime.timedelta(days=1)
tomorrow_str = tomorrow.strftime("%Y-%m-%d")
print(f"東京地方の{tomorrow_str}の天気は「{weather_tokyo[1]}」です。");
$ uv run my-weather-report-tool.py
Installed 9 packages in 31ms
東京地方の2025-03-18の天気は「くもり 昼前 から 昼過ぎ 晴れ 所により 昼過ぎ から 雨か雪」です。
自動的に依存パッケージのインストールとスクリプトの実行が行われました。小さなツールの速やかな実行に最適そうです。
スクリプトのヘッダに記載されている /// script
〜 ///
は何?
スクリプトのヘッダに書かれているPythonランタイムや依存性に関わるコメント /// script
〜 ///
はuv特有のメタデータではなく、Python Packaging内の inline-script-metadata という仕様によるメタデータです。
# /// script
# requires-python = ">=3.13"
# dependencies = [
# "datetime",
# "requests",
# ]
# ///
各セクションを # /// セクション名
〜 # ///
のコメントで挟んで表現します。
今回使っているscript
セクションであれば、スクリプトの依存性やツールバージョンを示すことができます。
inline-script-metadataは以下のコマンドで追記できるほか、手で追記しても構いません。
# inline-script-metadata を追加する場合
$ uv init my-weather-report-tool.py
# inline-script-metadata 上に依存性を追加する場合(別にinitしていなくてもOK)
$ uv add --script my-weather-report-tool.py datetime requests
依存性をコントロールする
依存性について細かいバージョンを指定することも可能です。
# dependencies = [
# "datetime",
# "requests >= 2.8.1",
# ]
単純に構成を保証するためにロックファイルが欲しい場合は以下のコマンドで スクリプト名.py.lock
を得ることができます。
$ uv lock --script my-weather-report-tool.py
# => my-weather-report-tool.py.lock が生成される
Pythonバージョンをコントロールは .python-version
で行う
仮に特定のバージョンのPythonで実行してほしい場合は以下のように指定したいところですが、単純にPythonランタイムに対する希望を指定するもので、Python(CPython)を自動インストールしてくれるようではないようです。
# # ↓ これを指定しても別にCPython 3.11.11 がインストールされるわけではない
# requires-python = "==3.11.11"
こういった場合には inline-script-metadata ではなく .python-version
ファイルを置いておくことにより uv run
する前に自動的にインストールしてくれます。
3.11.11
# uvで実行するだけで勝手に .python-version に基づいてCPythonがインストールされる
$ uv run my-weather-report-tool.py
普通のPythonプロジェクトにuvを使う
1ファイルで完結するスクリプトではなく、がっつり複数ファイルで構成された普通のPythonプロジェクトを構成したい場合にどうやって使うのか見ていきます。
まずプロジェクトを uv init
コマンドで作成します。Pythonでは定番のファイルが作成されます。
$ uv init mypj
$ ls -a mypj
.python-version README.md main.py pyproject.toml
基本的には依存性などプロジェクトのメタデータは pyproject.toml
に記述すればよいです。中身としては先ほどのシングルファイルのヘッダコメントの inline-script-metadata と同じようなものです。
dependencies
に依存性を追記し main.py
を実行してみます。
[project]
name = "mypj"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
# `uv add` コマンドでも追加できる
dependencies = ["datetime", "requests"]
import requests
import datetime
report_source = "https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json"
if __name__ == "__main__":
response = requests.get(report_source)
data = response.json()
for area in data[0]['timeSeries'][0]['areas']:
if area['area']['name'] == '東京地方':
weather_tokyo = area['weathers']
break
today = datetime.datetime.now()
tomorrow = today + datetime.timedelta(days=1)
tomorrow_str = tomorrow.strftime("%Y-%m-%d")
print(f"東京地方の{tomorrow_str}の天気は「{weather_tokyo[1]}」です。");
$ uv run main.py
Using CPython 3.13.2
Creating virtual environment at: .venv
Installed 9 packages in 28ms
東京地方の2025-03-18の天気は「くもり 昼前 から 昼過ぎ 晴れ 所により 昼過ぎ から 雨か雪」です。
以下を一度にやってくれました。
-
.python-version
に基づき CPython をインストール -
.venv
ディレクトリ(エディタ等が補完などのために使います)を作成 -
uv.lock
(依存性ロックファイル)を作成 -
main.py
を実行- スクリプトの実行が不要で依存性だけ持ってきたい場合は
uv sync
コマンドを使えばOK
- スクリプトの実行が不要で依存性だけ持ってきたい場合は
とにかく一度構成された通常プロジェクトでも、スクリプトの実行には uv run
を使えば問題なさそうです。
DockerイメージやCI/CDでuvを実行してみる
それぞれガイドがあるので、それに従えばいいでしょう。(CI/CDはGitLabを例に貼っています)
一応概要を書いておくと、以下のようになります。
- 公式のDockerイメージが
ghcr.io/astral-sh/uv
としてPythonバージョン等ごとに提供されています。 - 依存性のキャッシングや最適化は各ガイドで考慮されています。以下の例を見るとポイントが分かりやすいと思います。
- Docker
Dockerfile
# キャッシングありでの依存性のみのインストールレイヤーの作成 RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv sync --frozen --no-install-project # あらためてプロジェクトソースを追加して依存性インストール ADD . /app RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --frozen
- CI/CD
.gitlab-ci.yml
task-using-uv: variables: # uv の依存性をインストールするディレクトリを指定 (各uvコマンドで参照される) UV_CACHE_DIR: .uv-cache cache: - key: files: - uv.lock paths: - $UV_CACHE_DIR script: # 依存性をインストール - uv sync --frozen # プロジェクト内の検証タスク # - ... # 依存性ディレクトリをキャッシングのために最適化する (ビルドの最後でいいかも) - uv cache prune --ci
- Docker
古いAnsibleしか使えないプロジェクトにuvを使う
塩漬けになっているサーバなど、サーバのOSやPythonバージョンが古すぎるせいで、新しいAnsibleバージョンが使えない場合があります。
たとえば、Python 2.7しか無いCentOS 7 (EOL)のサーバをAnsibleで保守することを考えた場合、 以下のバージョンを最後にAnsibleによるサポートが終了していることがわかります。
Ansibleバージョン:
2.16
クライアントマシン: Python3.10
-3.12
ターゲットマシン: Python2.7
, Python3.6
-3.12
, Powershell5.1
Releases and maintenance
クライアントマシンのプラットフォームでインストールされているAnsibleバージョンは2025-03現在であれば 2.18
ではないでしょうか? 2.17
以降でPython 2.7しかないサーバに繋ぐとエラーで終了してしまいます。
こういったケースのために uv を使って塩漬けPJのAnsible/Pythonのバージョンを下げる手段があります。
uvでプロジェクト固有バージョンのAnsibleをインストールして利用する
まずは uv init .
コマンドで既存のAnsibleプロジェクトにPythonプロジェクトファイルを追加しましょう。(別に手でファイルを追加してもいいです)
$ uv init .
# 不要ファイルは適宜消す
$ rm main.py
$ ls -a
.python-version README.md pyproject.toml site.yml
各ファイルに必要事項を記入していきます。
[project]
name = "myansible"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
# Ansible動作環境としての条件を指定
requires-python = ">=3.10"
dependencies = ["ansible==9.11.0"]
3.12
uv venv
/ uv sync
コマンドでAnsibleを実行可能にします。
# .venv/ ディレクトリの作成
$ uv venv
Using CPython 3.12.9 interpreter at: /opt/homebrew/opt/python@3.12/bin/python3.12
Creating virtual environment at: .venv
Activate with: source .venv/bin/activate
# .venv/bin 内に ansible 系のコマンドを準備
$ uv sync
Resolved 11 packages in 82ms
Prepared 6 packages in 613ms
Installed 10 packages in 347ms
+ ansible==9.11.0
+ ansible-core==2.16.14
+ cffi==1.17.1
+ cryptography==44.0.2
+ jinja2==3.1.6
+ markupsafe==3.0.2
+ packaging==24.2
+ pycparser==2.22
+ pyyaml==6.0.2
+ resolvelib==1.0.1
venv を有効にします。
# `uv venv` で指定されたコマンドを実行
$ source .venv/bin/activate
いよいよ ansible
系コマンドにパスが通っている状態なので実行してみます。
(myansible) $ python --version
Python 3.12.9
(myansible) $ ansible --version
ansible [core 2.16.14]
config file = None
configured module search path = ['/Users/ksaitou/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /Users/ksaitou/rnd/2025-03-17_tryout_uv/myansible/.venv/lib/python3.12/site-packages/ansible
ansible collection location = /Users/ksaitou/.ansible/collections:/usr/share/ansible/collections
executable location = /Users/ksaitou/rnd/2025-03-17_tryout_uv/myansible/.venv/bin/ansible
python version = 3.12.9 (main, Feb 4 2025, 14:38:38) [Clang 16.0.0 (clang-1600.0.26.6)] (/Users/ksaitou/rnd/2025-03-17_tryout_uv/myansible/.venv/bin/python3)
jinja version = 3.1.6
libyaml = True
(myansible) $ ansible-playbook site.yml
TASK [Say hello] ********************************************
ok: [localhost] => {
"msg": "Hello"
}
プロジェクトでAnsibleを実行する前にvenvの有効化など多少の儀式は必要ですが、古いAnsibleプロジェクトの環境整備にuvが使えることもわかりました。
まとめ
Pythonのパッケージマネージャツールである uv を入れておけば、事前準備無しで、シングルファイルの実行から通常のPythonプロジェクトのマネージメント、また古いAnsibleプロジェクトの実行まで幅広いタスクをこなせることを確認できました。
とにかくPython製の開発用適当ツールの実行については pip3
/python3
コマンド (あるいは pip
/python
コマンド) の存在を一旦忘れて uv run
だけを使う という具合にやってしまってもいいかもなあと思っています。