---
# System prepended metadata

title: Python 程式碼品質管理工具 - Astral Ruff
tags: [06-Python]

---

# 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 與未使用的變數

![image](https://hackmd.io/_uploads/rJJeudEFbl.png =500x)

透過 `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 套件

![image](https://hackmd.io/_uploads/S1i1C_Etbx.png =500x)

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 %}
