uvで学ぶPythonの基礎
はじめに
いろいろありまして、Python やることになりました。
ちょっと前には、FastAPI の記事を書いたり、第二新卒の社員向けに Python でプログラミングの基礎を教えてたりしたんですが、最近は、uv らしいというのを見かけて、uv って何?ということで勉強しはじめたら、知らないことばかりで、いまさらではあるのですが、これはまずいということで基礎から学ばないと…という記事です。
まなぶ
とりあえず、本家のドキュメント読め。が鉄則ですよね。Introduction、Getting started から Guide の Running scripts で仮想環境がぁというあたりから怪しくなってきたので、Concepts をしっかりやろうと思います。
uv の中の人は(たぶん、uv の普通のユーザーさんも)、Project が興味の対象と思うので、Project からになってますが、自分としては、Python versions やらないとスッキリしなかったので、Python versions からにします。
Python
Python のある特定のバージョンとは、Python インタープリターと標準ライブラリとその他のサポートファイルで構成されます。
Python = python インタープリター + 標準ライブラリ + その他のサポートファイル?
その他のサポートファイルには、サードパーティパッケージやエントリポイントスクリプトが含まれるようです。ここで、1 つの Python の環境には、同じパッケージの複数のバージョンをインストールすることができないという制約とどう付き合うかということがポイントになってきます。
uv は、uv が管理する Python のインストール(managed)とそれ以外の Python のインストール(system)を検出できます。
これらの Python のインストールを使って、仮想環境を作るというのが、環境には同じパッケージの複数のバージョンをインストールすることができない問題の回避方法なので、当然のように仮想環境を前提とした話になっているのですね。
Python には、複数のディストリビューションがあることは知っていましたが、CPython の公式版がないということは知りませんでした。uv では、Astral 社のビルドを使用して managed の Python インストールが行われます。
デフォルトでは、system があれば、managed をダウンロードはしないようなので、絶対に managed を使いたいような場合は、python-preference
で only-managed
を設定します。
Project
プロジェクトとは、複数のファイルにまたがる Python のコードの管理のためにあり、pyproject.toml
ファイルで依存関係を定義できます。
pyproject.toml
には、最低、名前とバージョンが含まれます。
[project]
name = "example"
version = "0.1.0"
uv.lock
は、プロジェクト環境にインストールされている解決済みの正確なバージョンが含まれているので、バージョン管理して、再現可能なインストールに使用できます。uv.lock
は、人間が読める TOML ファイルですが、手動での編集はしてはいけません。また、このファイルの Python 標準はなく、uv 固有なので、開発チームのツールは揃えることになりそうです。
アプリケーションとライブラリとパッケージ
アプリケーションは、main.py が置かれるので、Webサーバー、スクリプト、CLI とかでは、デフォルトのままか明示的に --app
フラグを指定して uv init
で作ります。
パッケージ・アプリケーションは、パッケージをビルドするアプリケーションなので、PyPI などに公開する場合は、--package
フラグを指定して作ります。
ライブラリは、他のプロジェクトに使ってもらうものなので、パッケージとしてビルドすることになります。--lib
フラグを指定して作ります。
パッケージ化しないアプリケーション以外(つまり、パッケージ化するアプリケーションとライブラリ)は、src
ディレクトリにソースコードが配置される src
レイアウトが採用されます。これは、プロジェクトのトップレベルのディレクトリにソースコードを配置した場合に、カレントディレクトリがインポート対象となるのを防ぐ効果があるためです。
依存関係の定義
依存関係を定義するフィールドは、以下の4つです。
-
project.dependencies
: 公開された依存関係 -
project.optional-dependencies
: 公開されたオプションの依存関係 -
dependency-groups
: 開発のためのローカル依存関係 -
tool.uv.sources
: 開発中の依存関係の代替ソース
uv add
と uv remove
でも指定できるけど、pyproject.toml
を手編集しても OK。
上記の2番目以降は、--dev
、--group
、--optional
フラグを使用します。
依存関係の指定は、PEP508 で行います。
tool.uv.sources
は、uv 独自ですが、index
、git
、url
、path
、workspace
がサポートされているので、開発期間中に参照先と同時に更新できるので便利だと思います。
workspace は、複数のパッケージを管理でき、各パッケージは独自の pyproject.toml
を定義しますが、ワークスペースは単一のロックファイルを共有するので、ワークスペース全体が一貫した依存関係セットで動作します。これもモノレポだったり、関係するプロジェクトが多い場合に便利そうです。
project.optional-dependencies
は、package[<extra>]
指定の依存関係を定義するというのも知らずに、これまでは、エクストラ指定を記事や README 書かれているとおりに指定していたので、自分が何をしていたのか、かなりスッキリしました。
dependency-groups
は、PEP735 で定義されています。dev
グループはでデフォルトで、linter やテストツールなど開発時のみ必要なローカルの依存関係を入れておくのが普通の使い方のようです。
プロジェクトがパッケージの場合、ビルドの依存関係は、PEP518 に従って、build-system.requires
で指定します。
編集可能な依存関係は、編集中のソースコードとインストールされた wheel
の違いを解決するため、仮想環境内にプロジェクトへのリンク(.pth
ファイル)を追加するもので、uv は、デフォルトでワークスペース内のパッケージに編集可能なインストールを使用します。
コマンドの実行
プロジェクトで作業する場合、パッケージは .venv
の仮想環境にインストールされます。この環境はデフォルトでは、現在のシェルから呼び出される python インタプリタとは分離されているので、python
ではなく、uv run
を使用します。
インラインメタデータを宣言するスクリプトは、プロジェクトから分離された環境で自動的に実行されるそうです。
ロックと同期
ロックとは、プロジェクトの依存関係をロックファイルに解決するプロセスです。
同期とは、ロックファイルからパッケージのサブセットをプロジェクトの環境にインストールするプロセスです。
ロックと同期は、uv では自動です。例えば、uv run
を使用すると、要求されたコマンドを呼び出す前にプロジェクトがロックされ、同期されます。これによって、プロジェクトの環境は常に最新の状態になります。
プロジェクトの設定
project.requires-python
フィールドで、プロジェクトでサポートされている Python のバージョンを宣言できます。
project.scripts
で、CLI のエントリポイントを指定できます。
project.gui-scripts
で、GUI のエントリポイントを指定できます。
project.entry-points
で、プラグインのエントリポイントを指定できます。
build-system
で、ビルドシステムを宣言および構成できます。パッケージ化が必要な場合は、tool.uv.package = true
、不要な場合は、tool.uv.package = false
とすると build-system
の設定を上書きします。
tool.uv.conflicts
で、競合する依存関係のエラーを回避できますが、同じパッケージの違うバージョンを同時にインストールすることはできません。
ディストリビューションのビルド
基本は PyPI などのパッケージ・インデックスにアップロードするものがディストリビューション。
Python プロジェクトは、通常、ソースディストリビューション(sdists)とバイナリディストリビューション(wheels)の両方として配布されます。前者は、通常、プロジェクトのソースコードと追加のメタデータを含む .tar.gz
または .zip
ファイルであり、後者は、直接インストールできるビルド済みの成果物を含む .whl
ファイルです。
uv build
すると、カレントディレクトリのプロジェクトをビルドして、ビルド成果物を dist/
ディレクトリに配置します。uv build
は、最初にソースディストリビューションをビルドして、そこからバイナリディストリビューションをビルドします。--sdit
と --wheel
フラグでビルド対象を指定することもできます。
ワークスペース
ワークスペースとは、「一緒に管理されるワークスペースメンバーと呼ばれる1つ以上のパッケージのコレクション」です。
はじめる
ワークスペースを作成するには、pyproject.toml
に tool.uv.workspace
テーブルを追加します。これによって、そのパッケージをルートとするワークスペースが暗黙的に作成されます。また、既存のパッケージ内で uv init
を実行すると、新しく作成されたメンバーがワークスペースに追加され、ワークスペースルートに tool.uv.workspace
テーブルが無ければ追加されます。
ソース
ワークスペース内のワークスペースメンバーへの依存関係は、以下のように、tool.uv.sources
で設定されます。
[project]
name = "albatross"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["bird-feeder", "tqdm>=4,<5"]
[tool.uv.sources]
bird-feeder = { workspace = true }
[tool.uv.workspace]
members = ["packages/*"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
レイアウト
最も一般的なワークスペースレイアウトは、一連のライブラリを備えたルートプロジェクトと考えることができます。
albatross
├── packages
│ ├── bird-feeder
│ │ ├── pyproject.toml
│ │ └── src
│ │ └── bird_feeder
│ │ ├── __init__.py
│ │ └── foo.py
│ └── seeds
│ ├── pyproject.toml
│ └── src
│ └── seeds
│ ├── __init__.py
│ └── bar.py
├── pyproject.toml
├── README.md
├── uv.lock
└── src
└── albatross
└── main.py
使うか使わないか
ワークスペースは、単一のリポジトリ内で相互に接続された複数のパッケージの開発を容易にすることを目的としています。コードベースの複雑さが増すにつれて、コードベースを、それぞれ独自の依存関係とバージョン制約を持つ、より小さく構成可能なパッケージに分割すると便利です。
ワークスペースは、メンバーの要件(requirements)が競合する場合や、メンバーごとに別の仮想環境が必要な場合には適していません。この場合、パス依存関係が好ましいことがあります。
Tool
ツールとは、コマンドラインインターフェースを提供する Python パッケージです。
uv には、ツールと対話するための専用インターフェースが含まれています。ツールは、uv tool run
(別名 uvx
)を使用してインストールせずに呼び出すことができます。この場合、ツールの依存関係は、現在のプロジェクトから分離された一時的な仮想環境にインストールされます。
ツールは、uv tool install
でインストールすることもできます。この場合、ツールの実行可能ファイルは PATH で利用可能になります。分離された仮想環境はこの場合も使用されますが、コマンドが完了しても削除されません。
ほとんどの場合、ツールをインストールするよりも、uvx
を使用してツールを実行するほうが適切です。ツールをインストールするのは、システム上の他のプログラムでもツールを使用できるようにする必要がある場合などです。
デフォルトでは、uv ツールのディレクトリの名前は tools
で、uv アプリケーション状態ディレクトリ(例:~/.local/share/uv/tools
)にあります。ツール環境は、ツールパッケージと同じ名前のディレクトリ(例:.../tools/<name>
)に配置されます。
ツール環境は、uv tool upgrade
によって更新することも、uv tool install
で完全に再作成することもできます。
実行ファイルは、以下の環境変数のディレクトリにインストールされます。
$UV_TOOL_BIN_DIR
$XDG_BIN_HOME
$XDG_DATA_HOME/../bin
$HOME/.local/bin
ツールをインストールしても、以前に uv によってインストールされなかった bin
ディレクトリ内の実行可能ファイルは上書きされません。--force
フラグを使用すると、この動作を上書きできます。
依存関係の解決
Resolution とは、requirements のリストを取得し、その requirements を満たすパッケージバージョンのリストに変換するプロセスです。
詳しくは、見出しのリンクのコンセプトのページとリファレンスをお読みくださいなのですが、いくつかメモしておきます。
特定の依存関係に互換性があるバージョンを選択する場合、uv は、デフォルトで、サポートされている各 Python バージョンに対して最新の互換性のあるバージョンを選択しようとします。例えば、プロジェクトの requires-python
が >=3.8
で、依存関係の最新バージョンが Python 3.9 以降を必要とし、それ以前のバージョンはすべて Python 3.8 をサポートしている場合、リゾルバは、Python 3.9 以降を実行しているユーザーには、その最新バージョンを選択し、Python 3.8 を実行しているユーザーには、以前のバージョンを選択します。
依存関係の requires-python
の範囲を評価する際、uv は下限のみを考慮し、上限は完全に無視します。例えば、>=3.8, <4
は、>=3.8
として扱われます。
デフォルトでは、uv は各パッケージの最新バージョンを使用しようとします。例えば、uv pip install flask>=2.0.0
は Flask の最新バージョン(3.0.0 など)をインストールします。
--resolution lowest
を使用すると、uv は、直接および間接(推移的)の両方の依存関係に対して可能な限り低いバージョンをインストールします。また、--resolution lowest-direct
は、すべての直接依存関係に対して互換性のある最低バージョンを使用し、それ以外のすべての依存関係に対して互換性のある最新バージョンを使用します。
tool.uv.dependency-metadata
テーブルを使用すると、静的メタデータを事前に提供できるので、uv はビルドステップをスキップし、代わりに提供されたメタデータを使用できるので、パッケージのメタデータの解決にコストが大きい場合のビルドステップを省略できます。
uv は、特定の日付よりも前に公開されたディストリビューションのみに解決を制限し、新しいパッケージのリリースに関係なくインストールを再現できる --exclude-newer
オプションをサポートしています。
キャッシュ
依存関係のキャッシュ
uv は、積極的にキャッシュを使用して、以前の実行で既にアクセスされた依存関係の再ダウンロード(および再構築)を回避します。
- レジストリ: PyPI などからダウンロードした場合、HTTP キャッシュヘッダーを尊重します。
- URL: HTTP キャッシュヘッダーを尊重し、URL に基づいてキャッシュします。
- Git: Gitコミットハッシュに基づいてキャッシュします。
- ローカル: アーカイブの最終更新時刻、ディレクトリの
pyproject.toml
などのファイルの最終更新時刻に基づいてキャッシュします。
動的メタデータ
デフォルトでは、ディレクトリルートの pyproject.toml
、setup.py
、setup.cfg
ファイルが変更された場合、または、src
ディレクトリが追加または削除された場合にのみ、uv は、ローカルディレクトリの依存関係(編集可能ファイルなど)を再構築して再インストールします。
キャッシュの安全性
同じ仮想環境に対しても、複数の uv コマンドを同時に実行しても安全です。uv のキャッシュはスレッドセーフかつ追加専用に設計されているため、複数の同時読み取りおよび書き込みに対して堅牢です。uv は、プロセス間での同時変更を回避するために、インストール時にターゲット仮想環境にファイルベースのロックを適用します。
他の uv コマンドの実行中に uv キャッシュを変更すること(例: uv cache clean
)は安全でないことに注意してください。また、キャッシュを直接変更すること(例: ファイルまたはディレクトリを削除すること)は決して安全ではありません。
キャッシュディレクトリ
uv は、次の順序でキャッシュディレクトリを決定します。
-
--no-cache
が指定された場合の一時キャッシュディレクトリ -
--cache-dir
、UV_CACHE_DIR
、tool.uv.cache-dir
で指定されたディレクトリ - システムに適したディレクトリ(例:
$XDG_CACHE_HOME/uv
や$HOME/.cache/uv
)
パフォーマンスのためには、キャッシュディレクトリを uv が動作している Python 環境と同じファイルシステムに配置することが重要です。そうしないと、uv はキャッシュから環境にファイルをリンクできないので、代わりに低速のコピー操作にフォールバックします。
おわりに
いかがでしたでしょうか。
Python は1行も書きませんでしたが、最新の Python について知らないことが多く、一通りコンセプトを読むことで uv とともに Python の理解が進んだものと信じたいです。
ということで、しばらく uv で Python をやっていきたいと思います。
Discussion