# Python 程式碼品質管理工具 - Astral Ruff
## 1. Ruff 介紹
Ruff 是 [Astral](https://github.com/astral-sh) 團隊開發的一個 Python 的程式碼檢查工具,整合了 Lint、Formatter、Import 排序(isort) 的工具,並且透過 Rust 實作,速度飛快。(對一般小型專案開發,速度差異可能不明顯,但對大型專案或 CI/CD pipeline,能大幅縮短檢查時間)
### 核心功能
| 工具 | 特點 | 備註 |
| -------- | -------- | -------- |
| flake8 | 經典 Python Linter <font color="orange">*[1]*</font> | 需多個 plugin 才能完整檢查 |
| black | Formatter | 只能排版,不檢查 lint |
| isort | Import 排序 | 只排序 import |
| **Ruff** | **一次整合所有功能** | **高速、可替代 flake8 + black + isort** |
> <font color="orange">[1]</font>: Python Linter是一個靜態程式碼分析工具,在不執行程式的情況下,自動檢查程式碼中的語法錯誤、潛在漏洞、不規範的編程風格(Ex: PEP 8)以及不一致的縮排。
## 2. 為何需要?
### 2-1. 關於團隊協作開發
在團隊協作開發中,每個人的程式撰寫習慣不同,容易導致:
- 程式碼風格不一致
- 潛在 bug 難以及時發現
- PR review 時浪費時間在格式問題上
雖然單獨使用 flake8、black 或 isort 也能達成部分檢查或排版功能,但:
- 需要多個工具組合才能完整覆蓋 lint + formatter + import 排序
- 執行速度慢,對大型專案或 CI pipeline 影響明顯
因此,選擇一個整合 Lint、Formatter、Import 排序、且速度極快的工具,對提升開發效率、統一團隊程式碼品質,是更好、CP值更高的實務做法。
### 2-2. 關於 CI/CD workflow
在建立自動化工作流時,Coding Style 還只是冰山一角,但不能沒有它。
一套穩固的 CI/CD pipeline,最終目標是讓程式碼從開發到部署都能自動化、可預期、可追蹤。但這一切的前提是**程式碼本身要有一致的品質基準**,否則自動化只是把混亂加速。
Ruff 這類工具扮演的角色,是在 pipeline 的最前端建立第一道防線。當每一個 PR 進來,透過 CI 可以先確認:
- 程式碼風格是否一致
- 是否有明顯的潛在問題
- import 是否整潔有序
這些檢查通過後,後續的 test、build、deploy 才有意義。若連基本的 Coding style 都無法統一,test 結果的可信度、build 的穩定性、甚至 code review 的效率都會受到影響。
- 對團隊而言: 導入 Ruff 並將其整合進 CI 的真正價值不只是「抓格式錯誤」,而是建立一個客觀的、自動執行的共同標準。
- 新成員加入時: 不需要靠口頭約定或人工提醒,規範本身就內建在流程裡,Clone 一個專案下來,透過設定檔定義,無需解釋。
- 長期好處: Codebase 保持可維護性、擴充性與降低技術債。
## 3. 開始使用
### 3-1. 安裝
推薦使用 [Astral uv](https://github.com/astral-sh/uv) 來對專案進行管理。uv 是一個以 Rust 語言開發的 Python 專案管理工具,比傳統 pip、virtualenv 等工具執行速度快上10-100x(官方提供之數據),且操非常之方便,具備了套件安裝、虛擬環境管理、Python 版本管理、切換版本..等功能,非常強大!
以下將透過 `uv` 指令進行操作,若不使用,可自行替換成 `pip`。
> 更多 uv 介紹,可參考另一篇文章 [Python 套件管理工具: Astral uv
](https://hackmd.io/@MV1MNu9pSWqPmTx9CRvHbA/rJJAiBpzWl)
初始化 uv
```bash
uv init # 初始化一個新的 uv 專案
```
安裝 Ruff
```bash
uv add ruff
```
### 3-2. 指令應用
第一章有提到 Ruff 核心功能包括了 flake8 的 Linter、black 的 format 與 isort 的 import 排序,我們一一對照:
| 工具 | 對應 Ruff 指令 | 備註 |
| -------- | -------- | -------- |
| flake8 | `ruff check` | 內建大量 plugin 規則(pyflakes、pycodestyle、isort 等) |
| black | `ruff format` | 格式化行為與 Black 高度相容 |
| isort | `ruff check --select I` | isort 規則內建在 check 裡,用 I 系列規則 |
以往需要裝三個工具、跑三個指令,現在 Ruff 一次搞定,且速度快非常多。
#### 3-2-1. ruff check
`ruff check` - 程式碼靜態分析
```bash
ruff check . # 檢查當前目錄
ruff check . --fix # 檢查後自動修正可修復的問題
ruff check . --watch # 監聽模式,檔案變更時自動重新檢查
```
假設程式碼中包含了未使用的 import 與未使用的變數

透過 `ruff check .` 會列出所有警告的位置與原因
```bash
xiu@MacBook-Air test-uv % uv run ruff check .
F401 [*] `fastapi.FastAPIError` imported but unused
--> main.py:2:30
|
1 | import uvicorn
2 | from fastapi import FastAPI, FastAPIError, HTTPException
| ^^^^^^^^^^^^
3 | from pydantic import BaseModel
|
help: Remove unused import: `fastapi.FastAPIError`
F841 Local variable `unused_var` is assigned to but never used
--> main.py:37:5
|
35 | @app.post("/items")
36 | def create_item(item: Item):
37 | unused_var = "imported but unused."
| ^^^^^^^^^^
38 | return {"created": item.name}
|
help: Remove assignment to unused variable `unused_var`
Found 2 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
```
輸出中包含兩個警告,規則代碼前綴 `F` 代表來自 **Pyflakes**<font color="orange"> [2]</font> 規則集:
| 規則代碼 | 說明 | 可自動修復 |
|---------|------|----------|
| `F401` | Import 了但從未使用的模組 | `--fix` 可自動移除 |
| `F841` | 變數被賦值但從未使用 | 手動刪除 |
最後一行的提示說明了目前發現 2 個錯誤,其中 1 個可透過 `--fix` 自動修復;若加上 `--unsafe-fixes` 則可嘗試修復另外標示為不安全的項目 (Ruff 不確定修復後是否會改變程式行為)
> <font color="orange">[2]</font>「**Ruff supports over 900 lint rules, many of which are inspired by popular tools like Flake8, isort, pyupgrade, and others.** Regardless of the rule's origin, Ruff re-implements every rule in Rust as a first-party feature.」
> Ruff 整合了不同程式碼檢查工具的錯誤規則,更多可以參考 [官方文件: Ruff/Rules](https://docs.astral.sh/ruff/rules/)
>> 也可參考本文 3-3. 章節中 [常用的 Ruff 規則來源分類](#常用的-Ruff-規則來源分類)
#### 3-2-2. ruff check --select I
`ruff check --select I` - Import 排序檢查(isort)
假設程式碼包含了許多 import 套件

isort 會將 import 依來源分成三個區塊,區塊之間以空行分隔:
```python
# 第一區塊:標準函式庫
import json
import logging
import os
from datetime import datetime
from typing import Dict, List, Optional, Tuple, Union
# 第二區塊:第三方套件
import uvicorn
from fastapi import Depends, FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
# 第三區塊:本地模組
from myapp.database import get_user, list_users
from myapp.models import Item, UserResponse
from myapp.utils import get_pagination, verify_token
```
上圖範例中,原本的 import 順序混亂(第三方與標準函式庫交錯),透過 `--fix` 修正後,Ruff 會自動依照上述規則重新排列,並在不同區塊之間補上空行。
> `--select I` 表示只啟用 `I` 系列規則(isort),不執行其他 lint 檢查。若平常已在 `pyproject.toml` 中設定 `select = ["I"]`,則直接執行 `ruff check . --fix` 即可。
> 更多可參考下一個章節 [自定義規則](#3-3-自定義規則)
#### 3-2-3. ruff format
`ruff format` - 程式碼格式化工具
```bash
ruff format . # 格式化當前目錄所有檔案
ruff format --check . # 僅檢查格式,不實際修改(常用於 CI Workflow)
ruff format --diff . # 顯示格式化前後的差異
```
`ruff format` 設計目標是作為 Black 的直接替代品,預設配置與 Black 一致:
| 項目 | 預設值 |
| ------ | -------- |
| 行長度 | `88` 字元 |
| 字串引號 | 雙引號 `"` |
| 縮排 | 4 個空格 |
| 換行符號 | LF (`\n`) |
| Magic Trailing Comma | 啟用(有尾隨逗號時自動展開換行) |
### 3-3. 自定義規則
透過 pyproject.toml 可以自訂義 Formatter 規則,並以不同區塊區分設定。
#### 3-3-1. [tool.ruff] 全域設定
```toml
[tool.ruff]
line-length = 79 # 每行最大字數 (以 PEP8 規範為例, 79 個字元)
indent-width = 4 # 縮排寬度
target-version = "py311" # 目標 Python 版本
extend-exclude = [".git", ".venv", "logs/"] # 忽略的檔案或目錄
```
- `line-length`:每行最大字元數,可依團隊或專案自行決定,PEP 8 建議為 79,Black 預設為 88,亦可依照團隊或專案自行設定。
- `target-version`:指定目標 Python 版本,會影響 `UP`(pyupgrade)規則的判斷,例如設為 `py311` 時,Ruff 只會建議 Python 3.11 以上才支援的新語法
- `extend-exclude`:額外排除不需要檢查的目錄或檔案,通常會排除虛擬環境與版本控制目錄
#### 3-3-2. [tool.ruff.lint] 檢查規則設定
```toml
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "N"] # 啟用的規則
ignore = ["E501"] # 忽略的規則
fixable = ["ALL"] # 允許自動修復的規則
unfixable = [] # 禁止自動修復的規則
# 允許在特定情況下使用的規則
per-file-ignores = { "tests/**" = ["F401"], "**/__init__.py" = ["F401"] }
```
`select` 中各規則集的對應說明
#### 常用的 Ruff 規則來源分類
| 代號 | 來源工具 | 說明 |
| :--------: | -------- | -------- |
| E | pycodestyle | PEP 8 風格規範 |
| F | Pyflakes | 邏輯錯誤,如未使用的 import、未定義的變數 |
| I | isort | Import 排序 |
| UP | pyupgrade | 建議升級為新版 Python 語法 |
| B | flake8-bugbear | 常見 bug 與設計問題 |
| N | pep8-naming | 命名慣例(函式、類別、變數命名) |
`per-file-ignores` 可針對特定路徑忽略特定規則,例如 `__init__.py` 通常會刻意 re-export 模組,因此忽略 `F401`(unused import)是合理的做法。
#### 自定義忽略警告
特定測試、`__init__` 或 migrations 目錄,可透過 `[tool.ruff.lint.per-file-ignores]` 針對路徑忽略特定規則
```toml
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["F401", "S101"] # 測試檔案忽略 unused import 與 assert
"**/__init__.py" = ["F401"] # init 檔案忽略 unused import
"migrations/**" = ["E501"] # migration 檔案忽略行長度
```
若只需要忽略 **單行** 的特定警告,可在該行尾端加上 `# noqa: [規則代號]` 註解
```python
QUERY_GET_USER = "SELECT id, name, email, created_at FROM users WHERE id = :user_id AND is_active = TRUE ORDER BY created_at DESC LIMIT 1;" # noqa: E501
```
兩種方法的適用情境:
| 方法 | 適用情境 |
|------|---------|
| `per-file-ignores` | 整個目錄或檔案類型有系統性的例外(如 migrations、tests) |
| `# noqa` | 單行的特殊情況,例如無法拆行的長字串、註解或 SQL 語法 |
> 建議優先使用 `per-file-ignores` 集中管理例外規則,`# noqa` 僅用於真正無法避免的個案,避免濫用導致問題被掩蓋。
#### 3-3-3. [tool.ruff.format] 格式化設定
```toml
[tool.ruff.format]
quote-style = "double" # 字串引號風格(double or single)
indent-style = "space" # 縮排風格(space or tab)
skip-magic-trailing-comma = false # 預設 False 若尾行有 ',' 則自動換行
line-ending = "lf" # 換行符號(lf / crlf / cr / native) 預設為 "lf"
```
format 排版規則中,通常建議配置:
- `quote-style = "double"`:統一使用雙引號,與 Black 預設一致,也是 Python 社群較常見的方法。
- `indent-style = "space"`:使用空格縮排,符合 PEP8 建議,避免 tab 在不同編輯器顯示不一致的問題。
- `skip-magic-trailing-comma = false`:保留尾隨逗號的換行控制權,讓開發者可以刻意鎖定多行格式,git diff 也更乾淨。
- `line-ending = "lf"`:統一使用 Unix 換行符號,避免跨平台(Windows / macOS / Linux)協作時產生多餘的 diff。
> 以上四項均為 Ruff 的預設值,與 Black 的行為一致。若團隊無特殊需求,可省略不寫;明確寫出的好處是讓設定檔具備**自我說明**的效果,新成員一眼就能了解專案的格式規範。
### 3-4. pyproject.toml 範例
```toml
[tool.ruff]
line-length = 88 # 每行最大字元數(Black 預設值)
indent-width = 4 # 縮排寬度
target-version = "py311" # 目標 Python 版本,影響 UP 規則的語法建議
extend-exclude = [".git", ".venv"] # 排除不需要檢查的目錄
[tool.ruff.lint]
select = [
"E", # pycodestyle - PEP 8 風格規範
"F", # Pyflakes - 邏輯錯誤(unused import、未定義變數等)
"I", # isort - Import 排序
"UP", # pyupgrade - 建議升級為新版 Python 語法
"B", # flake8-bugbear - 常見 bug 與設計問題
"N", # pep8-naming - 命名慣例
]
ignore = ["E501"] # 忽略行長度限制(由 line-length 統一管理)
fixable = ["ALL"] # 允許所有規則自動修復
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["F401", "S101"] # 測試檔案忽略 unused import 與 assert
"**/__init__.py" = ["F401"] # init 檔案忽略 unused import(re-export 用途)
[tool.ruff.format]
quote-style = "double" # 統一使用雙引號
indent-style = "space" # 使用空格縮排(非 tab)
skip-magic-trailing-comma = false # 有尾隨逗號時強制保持多行
line-ending = "lf" # 統一使用 LF 換行(跨平台一致性)
```
## 延伸閱讀
{%preview https://hackmd.io/@MV1MNu9pSWqPmTx9CRvHbA/rJJAiBpzWl %}