UbuntuでuvとRuffを使う|インストールからプロジェクト管理・lintまで
/ 21 min read
Table of Contents
はじめに
Pythonの環境構築は、venv で仮想環境を作り、pip でパッケージを入れ、black と flake8 と isort を別々に設定して……という手順が長らく定番でした。
これを uv(パッケージ・環境管理)と Ruff(linter・formatter)の2つに置き換えると、手数がかなり減ります。どちらもRust製で、同じ Astral 社が作っています。
この記事では、Ubuntuに両方を入れて実際に使うところまでを一通りまとめました。
検証環境は次のとおりです。
| 項目 | バージョン |
|---|---|
| OS | Ubuntu 24.04 LTS |
| uv | 0.12.9 |
| Ruff | 0.16.6 |
| Python | 3.12.3(システム) |
uvとRuffは何を置き換えるのか
先に対応関係を整理しておきます。
| 従来のツール | 置き換え先 | 役割 |
|---|---|---|
pip |
uv add / uv pip |
パッケージのインストール |
venv |
uv が自動管理 |
仮想環境の作成 |
pip-tools / poetry lock |
uv lock |
依存のロック |
pyenv |
uv python |
Pythonのバージョン管理 |
pipx |
uv tool / uvx |
CLIツールの導入 |
flake8 / pylint |
ruff check |
lint |
black |
ruff format |
フォーマット |
isort |
ruff check(I ルール) |
import順の整列 |
設定ファイルも pyproject.toml 1つにまとまります。setup.cfg と .flake8 と .isort.cfg を行き来する必要はありません。
uvをインストールする
公式インストーラを使う
Ubuntu 24.04 の apt には uv のパッケージがありません(apt-cache policy uv を叩くと何も出ません)。公式のインストールスクリプトを使うのが確実です。
curl -LsSf https://astral.sh/uv/install.sh | sh~/.local/bin/uv と ~/.local/bin/uvx が置かれ、シェルの設定ファイル(~/.bashrc など)に PATH を通す行が追記されます。
インストール直後は現在のシェルに反映されていないので、読み込み直します。
source ~/.bashrcuv --versionuv 0.12.9pipxを使う場合
apt で管理されたパッケージとして入れたいなら、pipx 経由という手もあります。pipx は Ubuntu 24.04 の universe リポジトリにあります。
sudo apt install pipxpipx ensurepathpipx install uvただし、この方法だと後述の uv self update が使えません(pipxが管理しているため)。更新は pipx upgrade uv になります。特別な理由がなければ公式インストーラのほうが素直です。
更新とアンインストール
# uv自身を最新版に更新uv self update
# アンインストールuv cache cleanrm -rf ~/.local/share/uvrm ~/.local/bin/uv ~/.local/bin/uvxRuffをインストールする
Ruffの入れ方は3通りあり、用途によって使い分けます。
| 方法 | コマンド | 向いている場面 |
|---|---|---|
| プロジェクトの開発依存 | uv add --dev ruff |
チームで同じバージョンを揃えたいとき |
| グローバルツール | uv tool install ruff |
どのディレクトリでも ruff を叩きたいとき |
| 都度実行(インストールなし) | uvx ruff check . |
一度きりの確認 |
普段のプロジェクト作業では uv add --dev ruff が基本です。バージョンが pyproject.toml と uv.lock に記録されるので、「自分の環境では通るのにCIで落ちる」が起きません。
グローバルに入れる場合は次のとおりです。
uv tool install ruffruff --versionruff 0.16.6uvx ruff は、その場でダウンロードして実行し、環境を汚しません。キャッシュが効くので2回目以降は一瞬です。
uvでプロジェクトを作る
uv init
uv init myprojectcd myproject生成されるのはこれだけです。
myproject/├── .gitignore├── .python-version├── README.md├── pyproject.toml└── src/ └── myproject/ └── __init__.pypyproject.toml の中身です。
[project]name = "myproject"version = "0.1.0"description = "Add your description here"readme = "README.md"requires-python = ">=3.13"dependencies = []
[build-system]requires = ["uv_build>=0.12.9,<0.13.0"]build-backend = "uv_build"Pythonのバージョンを指定したい場合は --python を渡します。
uv init myproject --python 3.12指定したバージョンが .python-version に書き出され、以降このディレクトリではそのバージョンが使われます。
パッケージを追加する
uv add pandasUsing CPython 3.12.3 interpreter at: /usr/bin/python3.12Creating virtual environment at: .venvResolved 6 packages in 261msInstalled 4 packages in 164ms + numpy==2.5.2 + pandas==3.0.5 + python-dateutil==2.9.0.post0 + six==1.17.0.venv の作成は自動です。python -m venv .venv も source .venv/bin/activate も要りません。ここが pip との一番大きな違いです。
uv add は3つのファイルを同時に更新します。
pyproject.toml…dependenciesに追記uv.lock… 依存ツリー全体をハッシュ付きで固定.venv/… 実体のインストール
開発時だけ使うものは --dev を付けます。
uv add --dev ruffpyproject.toml はこうなります。
[project]dependencies = [ "pandas>=3.0.5",]
[dependency-groups]dev = [ "ruff>=0.16.6",]削除は uv remove pandas です。
実行する
uv run main.pyHello from myproject!uv run は、実行前に uv.lock と .venv の同期を毎回確認します。pyproject.toml を手で書き換えた直後でも、uv run を叩けば足りないパッケージが入ってから実行されます。
仮想環境のPythonを直に使いたいときは uv run python です。
uv run pythonuv run python -c "import pandas; print(pandas.__version__)"依存関係を確認する
uv treemyproject v0.1.0└── pandas v3.0.5 ├── numpy v2.5.2 └── python-dateutil v2.9.0.post0 └── six v1.17.0どのパッケージが何を連れてきたかが一目で分かります。pip list では出ない情報です。
別マシンで環境を再現する
pyproject.toml と uv.lock をリポジトリに入れておけば、クローン先では次の1行で済みます。
uv syncCIなど、ロックファイルを絶対に更新させたくない場面では --frozen を付けます。
uv sync --frozenロックファイルが pyproject.toml と食い違っていればエラーで止まるので、意図しない依存の更新を検出できます。
requirements.txt が必要なとき
デプロイ先が requirements.txt しか受け付けない場合は、ロックファイルから書き出せます。
uv export --format requirements.txt --no-dev --no-hashes -o requirements.txtpandas==3.0.5 # via myprojectpython-dateutil==2.9.0.post0 # via pandassix==1.17.0 # via python-dateutil--no-hashes を外すと各パッケージの sha256 が付きます。改ざん検証が必要ならそちらを使ってください。
逆に、既存の requirements.txt から移行するときはこうです。
uv init --bareuv add -r requirements.txt--bare は pyproject.toml だけを作るオプションで、既存のディレクトリ構成を壊しません。
Pythonのバージョンを切り替える
uvは pyenv の役割も持っています。ビルドではなくビルド済みバイナリを落としてくるので、数秒で終わります。
# 使えるバージョンの一覧uv python list
# 3.13をインストールuv python install 3.13Downloading cpython-3.13.15-linux-x86_64-gnu (33.2MiB) Downloaded cpython-3.13.15-linux-x86_64-gnuInstalled Python 3.13.15 in 1.41spyenv install のようにソースからコンパイルしないため、libssl-dev や zlib1g-dev といったビルド依存を先に apt で入れる必要もありません。
一時的に別バージョンで動かすなら --python です。
uv run --python 3.13 python -VPython 3.13.15プロジェクトのバージョンを固定するときは .python-version に書くか、uv python pin 3.13 を実行します。
使い捨てスクリプトを書く
**1ファイルで完結するスクリプトなら、プロジェクトを作らずに依存を書けます。**PEP 723 のインラインメタデータという仕組みです。
uv init --script check.py --python 3.12uv add --script check.py requestsファイルの冒頭にコメント形式で依存が書き込まれます。
# /// script# requires-python = ">=3.12"# dependencies = [# "requests>=2.34.2",# ]# ///import requests
r = requests.get("https://httpbin.org/get", timeout=10)print(r.status_code)実行はそのまま uv run です。
uv run check.pyInstalled 5 packages in 5ms200**仮想環境はuvが裏で用意して破棄します。**このファイルを人に渡せば、相手はuvさえ入っていれば同じように動かせます。cronに置く小さな処理や、他人に投げるワンショットの検証スクリプトで便利です。
依存を書き込みたくない場合は --with でその場限りの追加もできます。
uv run --with rich python -c "import rich; rich.print('[bold]hello[/bold]')"Ruffでlintをかける
ここからRuffです。次のような雑なコードを用意します。
import osimport pandas as pdfrom datetime import datetime
def load( path ): df=pd.read_csv(path,dtype={"code":str}) unused = 1 return df
x = { 'a':1,'b':2 }チェックします。
uv run ruff check .デフォルトの出力は該当箇所と修正案まで表示する詳細形式です。件数だけ手早く見たいときは --output-format concise を使います。
uv run ruff check --output-format concise .sample.py:1:1: I001 [*] Import block is un-sorted or un-formattedsample.py:1:8: F401 [*] `os` imported but unusedsample.py:3:22: F401 [*] `datetime.datetime` imported but unusedsample.py:7:5: F841 Local variable `unused` is assigned to but never usedFound 4 errors.[*] 3 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).[*] が付いているものは自動修正できます。
uv run ruff check --fix .Found 4 errors (3 fixed, 1 remaining).F841(未使用のローカル変数)が残りました。これは削除すると挙動が変わる可能性があるため、安全でない修正として保留されています。承知のうえで直すなら --unsafe-fixes を付けます。
uv run ruff check --fix --unsafe-fixes .ルールの意味が分からないときは ruff rule で説明が読めます。
uv run ruff rule UP031# printf-string-formatting (UP031)
Derived from the **pyupgrade** linter....Ruffでフォーマットする
lintとは別コマンドです。ここが最初につまずくところで、ruff check --fix はコードスタイルを整えません。
uv run ruff format .1 file reformatted, 2 files left unchanged先ほどのコードはこうなります。
import pandas as pd
def load(path): df = pd.read_csv(path, dtype={"code": str}) return df
x = {"a": 1, "b": 2}black とほぼ同じ結果になります。差分を出すだけで書き換えたくないときは --diff、チェックのみなら --check です。
uv run ruff format --check .1 file would be reformatted, 3 files already formatted整形されていないファイルがあると終了コード1で落ちるので、CIやコミット前フックにそのまま使えます。
普段は ruff check --fix と ruff format の2つを続けて実行するのが基本の流れになります。
uv run ruff check --fix . && uv run ruff format .pyproject.tomlで設定する
Ruffの設定は pyproject.toml に書きます。専用の設定ファイル(ruff.toml)でも構いませんが、uvと同じファイルにまとめたほうが管理は楽です。
[tool.ruff]line-length = 100target-version = "py312"
[tool.ruff.lint]select = ["E", "F", "I", "UP", "B", "SIM", "RET", "PERF"]ignore = ["E501"]
[tool.ruff.format]quote-style = "double"select で指定するルール群のうち、よく使うものです。
| 記号 | 由来 | 内容 |
|---|---|---|
E/W |
pycodestyle | PEP 8 のスタイル |
F |
Pyflakes | 未使用のimport・変数、未定義名 |
I |
isort | importの並び順 |
UP |
pyupgrade | 古い書き方を新しい構文へ |
B |
flake8-bugbear | バグになりやすい書き方 |
SIM |
flake8-simplify | 冗長な条件式・ループの単純化 |
RET |
flake8-return | return 周りの整理 |
PERF |
Perflint | 遅くなりやすい書き方 |
ANN |
flake8-annotations | 型注釈の欠落 |
デフォルトは ["E4", "E7", "E9", "F"] と控えめです。最低限 I を足しておくとimportの整列が効くので、isortを別に入れる必要がなくなります。
この設定で先ほどのコードをチェックすると、検出されるものが増えます。
sample.py:1:8: F401 [*] `os` imported but unusedsample.py:7:9: PERF401 Use a list comprehension to create a transformed listsample.py:12:9: SIM115 Use a context manager for opening filessample.py:19:5: RET505 [*] Unnecessary `else` after `return` statementsample.py:24:12: UP031 Use format specifiers instead of percent formatファイル単位で例外を作りたい場合は per-file-ignores を使います。
[tool.ruff.lint.per-file-ignores]"__init__.py" = ["F401"]"tests/*" = ["ANN"]1行だけ黙らせたいときは # noqa です。ルール名まで書くのが作法で、そうしないと後から理由が追えなくなります。
from .models import User # noqa: F401 # マイグレーション検出のため読み込みが必要不要になった # noqa は RUF100 ルールで検出できます。
統計を出す
どのルールが何件出ているかを俯瞰したいときは --statistics です。既存プロジェクトにRuffを後から入れるとき、まずこれを見て select を決めます。
uv run ruff check --statistics .1 PERF401 [ ] manual-list-comprehension1 SIM115 [ ] open-file-with-context-handler1 UP031 [ ] printf-string-formatting1 RET505 [*] superfluous-else-return1 F401 [*] unused-import数千件出るようなら、いきなり全部を select せず F と I あたりから始めて、直せた分だけ追加していくのが現実的です。
VSCodeと連携する
拡張機能「Ruff」(charliermarsh.ruff)を入れて、.vscode/settings.json に次を書きます。
{ "[python]": { "editor.defaultFormatter": "charliermarsh.ruff", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll.ruff": "explicit", "source.organizeImports.ruff": "explicit" } }}保存するたびにフォーマットとimport整列と自動修正がかかります。拡張機能はRuff本体をバンドルしていますが、pyproject.toml の設定は読んでくれるので、CLIとVSCodeで結果が食い違うことはありません。
既存の環境から少しずつ移す
pip の使い方をそのまま持ち込みたい場合、uvには互換インターフェースがあります。
uv venvuv pip install requestsuv pip listuv venv で .venv を作り、uv pip install でそこに入れます。pyproject.toml も uv.lock も作られないので、既存のワークフローを壊さずに「pipが速くなっただけ」の状態から始められます。
慣れてきたら uv init --bare と uv add に移していけば十分です。いきなり全部を置き換える必要はありません。
つまずきやすいところ
**ruff check --fix と ruff format は別物です。**前者はlintの自動修正、後者はコード整形です。片方だけ流して「フォーマットされない」と悩むケースが多いので、2つセットで覚えてください。
source .venv/bin/activate は基本的に不要です。uv run を通せば仮想環境のPythonが選ばれます。ただし、uv run を挟めないツール(一部のIDE設定やデバッガ)に渡すときは、.venv/bin/python を絶対パスで指定します。
**uv.lock はコミットします。**アプリケーションなら必ず入れてください。逆に、ライブラリとして配布するパッケージではコミットしないのが通例です。
**isort を別に入れる必要はありませんが、select に I がないと動きません。**デフォルトの select には含まれていないので、明示的に足してください。
**uv add は最小バージョン制約(>=)で書き込みます。**バージョンを固定したいなら uv add "pandas==3.0.5" のように明示します。とはいえ、再現性は uv.lock が担保するので、通常は >= のままで問題ありません。
**Ruffはまだ 0.x です。**マイナーバージョンの更新でルールの挙動や既定値が変わることがあります。チーム開発では uv add --dev ruff でバージョンを固定し、CIと同じものを使ってください。
おわりに
uvとRuffを入れると、Pythonの環境まわりで触るコマンドが uv add / uv run / uv sync と ruff check / ruff format の5つ程度に収束します。
特に効くのは待ち時間が消えることです。pip install pandas で数十秒待っていたところが1秒以内に終わるので、「環境を作り直すのが面倒だから使い回す」という判断がなくなります。プロジェクトごとに環境を分ける習慣が、コストなしで維持できるようになりました。
まずは uvx ruff check . を既存プロジェクトで叩いてみるところからでも十分だと思います。何も入れずに現状を確認できます。
