你的程式碼能跑不代表它沒問題。這兩個工具幫你在「出事之前」就抓到問題。
問題的起點:Python 太自由了
Python 是一個非常「寬容」的語言。它讓你做很多事情,但不會告訴你做錯了:
def add(a: int, b: int) -> int:
return a + b
result = add("hello", "world") # Python 不會報錯,直接跑
print(result) # "helloworld" — 你說要 int,它收到 str,沒人管
import json # 你 import 了但沒用到,Python 不管
import os # 這個也沒用到,Python 也不管
def process(data ,count): # 逗號前面多了空格,Python 不管
temp = 42 # 定義了變數但沒用,Python 不管
result = data*count
return result
這些問題不會讓程式「爆掉」,但會慢慢腐蝕你的 codebase——直到某天出了 bug,你花了三小時 debug,結果發現只是型別傳錯了。
Ruff 和 Mypy 就是幫你在這些問題變成 bug 之前就抓到它們。
Mypy:型別檢查器
它解決什麼問題?
Python 的 type hint(a: int)只是給人看的註解,執行時完全不檢查。Mypy 是那個真的去檢查的工具。
沒有 Mypy 的世界
def get_user_name(user_id: int) -> str:
if user_id <= 0:
return None # 你說回傳 str,卻回傳了 None
return "Alice"
name = get_user_name(1)
print(name.upper()) # 大部分時候沒事
print(get_user_name(-1).upper()) # 💥 AttributeError: 'NoneType' has no attribute 'upper'
# 半夜三點,上線才爆
Python 不會警告你,直到程式跑到那一行才炸掉。
有 Mypy 的世界
$ mypy your_code.py
your_code.py:3: error: Incompatible return value type (got "None", expected "str")
程式還沒跑就告訴你哪裡錯了。 你可以這樣修:
def get_user_name(user_id: int) -> str | None: # 明確宣告可能回傳 None
if user_id <= 0:
return None
return "Alice"
name = get_user_name(1)
if name is not None: # Mypy 會強制你處理 None 的情況
print(name.upper())
Mypy 能抓到的錯誤
# ① 型別不匹配
def add(a: int, b: int) -> int:
return a + b
add("hello", "world")
# mypy: Argument 1 has incompatible type "str"; expected "int"
# ② 忘記處理 None
def find_user(user_id: int) -> dict | None:
...
user = find_user(1)
print(user["name"])
# mypy: Value of type "dict | None" is not indexable
# ③ 回傳值型別錯誤
def calculate_price(quantity: int, unit_price: float) -> int:
return quantity * unit_price # 回傳 float,但你說回傳 int
# mypy: Incompatible return value type (got "float", expected "int")
# ④ 缺少回傳值
def validate(value: int) -> bool:
if value > 0:
return True
# 忘了 else 的情況,隱式回傳 None
# mypy: Missing return statement
# ⑤ 存取不存在的屬性
class User:
def __init__(self, name: str):
self.name = name
user = User("Alice")
print(user.email)
# mypy: "User" has no attribute "email"
怎麼用
# 安裝
pip install mypy
# 檢查單一檔案
mypy your_code.py
# 檢查整個專案
mypy src/
# 嚴格模式(所有函式都必須有 type annotation)
mypy --strict src/
設定(在 pyproject.toml)
[tool.mypy]
python_version = "3.12"
strict = true # 開啟嚴格模式
warn_return_any = true # 回傳 Any 型別時警告
warn_unused_ignores = true # 不必要的 type: ignore 會警告
disallow_untyped_defs = true # 所有函式都必須有型別標註
# 如果某些第三方套件沒有 type stub,可以個別忽略
[[tool.mypy.overrides]]
module = "some_untyped_library.*"
ignore_missing_imports = true
Ruff:超快的 Linter + Formatter
它解決什麼問題?
在 Ruff 出現之前,你需要裝好幾個工具才能做到完整的程式碼品質檢查:
| 工具 | 做什麼 | 速度 |
|---|---|---|
pylint |
找 bug 和風格問題 | 慢 |
flake8 |
找風格違規 | 中等 |
isort |
自動排序 import | 中等 |
black |
自動格式化程式碼 | 中等 |
pyflakes |
找未使用的變數/import | 中等 |
pyupgrade |
自動升級舊語法 | 中等 |
bandit |
找安全性問題 | 中等 |
Ruff 用 Rust 寫成,把以上所有工具的功能合成一個,而且快 10~100 倍。大型專案裡 pylint 跑好幾分鐘的事情,Ruff 幾秒鐘就完成。
Ruff 當 Linter:幫你找問題
# bad_code.py
import os
import json # ← 沒用到
def calculate(x ,y): # ← 逗號前有多餘空格
temp = 42 # ← 變數定義了但沒用到
result = x+y
return result
class user: # ← class 名稱應該用 CapWords
pass
$ ruff check bad_code.py
bad_code.py:2:8: F401 `json` imported but unused
bad_code.py:4:18: E231 missing whitespace after ','
bad_code.py:5:5: F841 local variable `temp` is assigned but never used
bad_code.py:9:7: N801 class `user` should use CapWords convention
Found 4 errors.
自動修復能修的:
$ ruff check --fix bad_code.py
Found 4 errors (2 fixed, 2 remaining).
# 自動移除沒用到的 import、修正空格
# class 命名和未使用變數需要你自己改(因為 ruff 不確定你的意圖)
Ruff 當 Formatter:幫你統一風格
# 格式化前
def foo( a,b, c ):
return {"key":a,"key2": b,"key3":c}
x=[1,2,
3,4,5]
$ ruff format bad_code.py
1 file reformatted
# 格式化後
def foo(a, b, c):
return {"key": a, "key2": b, "key3": c}
x = [1, 2, 3, 4, 5]
Ruff 能抓到的常見問題
# F401: 未使用的 import
import os # 你沒用到 os,刪掉
# F841: 未使用的變數
result = compute() # 你算了但沒用到
# E711: 用 == 比較 None
if value == None: # 應該用 is None
# B006: Mutable default argument
def foo(items=[]): # 應該用 None
# UP035: 過時的 import
from typing import List # Python 3.9+ 直接用 list
# N801: class 命名不符合 CapWords
class my_class: # 應該是 MyClass
# I001: import 沒有按照順序排列
import os
import abc # abc 應該在 os 前面
# S101: 在非測試程式碼中使用 assert(安全性問題)
assert user.is_admin # 用 raise 而不是 assert
怎麼用
# 安裝
pip install ruff
# Lint:找出問題
ruff check .
# Lint + 自動修復
ruff check --fix .
# Format:自動統一風格
ruff format .
# 查看會改什麼(不實際修改)
ruff format --check .
設定(在 pyproject.toml)
[tool.ruff]
line-length = 80 # 每行最長 80 字元
target-version = "py312" # 目標 Python 版本
[tool.ruff.lint]
select = [
"E", # pycodestyle errors(空格、縮排等)
"F", # pyflakes(未使用的 import、變數等)
"I", # isort(import 排序)
"N", # pep8-naming(命名規範)
"UP", # pyupgrade(用新語法取代舊的)
"B", # flake8-bugbear(常見 bug 模式)
"S", # bandit(安全性問題)
"SIM", # flake8-simplify(可簡化的程式碼)
]
# 想要更嚴格?直接全開
# select = ["ALL"]
ignore = [
"S101", # 允許在測試中使用 assert
]
[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101"] # 測試檔案允許 assert
Ruff vs Mypy:各管什麼?
這兩個工具是互補的,不是替代關係:
Ruff Mypy
┌──────────────────┐ ┌──────────────────┐
│ 風格和格式 │ │ 型別正確性 │
│ ─────────────── │ │ ─────────────── │
│ ✓ import 排序 │ │ ✓ 型別不匹配 │
│ ✓ 未使用的變數 │ │ ✓ 忘記處理 None │
│ ✓ 命名規範 │ │ ✓ 回傳型別錯誤 │
│ ✓ 空格/縮排 │ │ ✓ 屬性不存在 │
│ ✓ 安全性問題 │ │ ✓ 參數型別錯誤 │
│ ✓ 過時語法 │ │ │
└──────────────────┘ └──────────────────┘
看得整不整齊 用得對不對
一個實際的例子:
import json # ← Ruff 會抓(F401: 未使用的 import)
def add(a: int, b: int) -> int:
return a + b
result = add("hello", "world") # ← Mypy 會抓(型別不匹配)
temp = 42 # ← Ruff 會抓(F841: 未使用的變數)
兩個都用,才是完整的品質檢查。
整合 Git Pre-commit Hook
為什麼需要?
你可以手動跑 ruff check 和 mypy,但你會忘。特別是趕進度的時候。
Pre-commit hook 的意思是:每次你執行 git commit,自動先跑這些檢查。沒通過就不讓你 commit。
git commit
│
├── ① ruff check(lint) → 有未使用的 import?❌ 擋住
├── ② ruff format(格式化) → 格式不對?自動修好,要你重新 commit
└── ③ mypy(型別檢查) → 型別錯誤?❌ 擋住
│
✅ 全部通過 → commit 成功
這樣不管你多趕,品質都有底線。
Step 1:安裝 pre-commit
pip install pre-commit
Step 2:建立 .pre-commit-config.yaml
在你的專案根目錄建立這個檔案:
# .pre-commit-config.yaml
repos:
# Ruff:Lint + Format
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.1 # 用最新版本,可以跑 `pre-commit autoupdate` 自動更新
hooks:
# 先跑 linter(並自動修復能修的問題)
- id: ruff-check
args: [--fix]
# 再跑 formatter
- id: ruff-format
# Mypy:型別檢查
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.15.0 # 用最新版本
hooks:
- id: mypy
args: [--strict, --ignore-missing-imports]
# 如果你的程式碼用到第三方套件的型別,加在這裡
additional_dependencies:
- pydantic
- types-requests
注意順序:ruff-check(帶 --fix)要在 ruff-format 前面。 因為 lint 修復可能產生需要再格式化的程式碼。
Step 3:啟用 hook
# 安裝 hook 到 .git/hooks/
pre-commit install
# 完成!之後每次 git commit 都會自動跑
Step 4:試跑看看
# 手動對所有檔案跑一次(建議第一次設定時執行)
pre-commit run --all-files
輸出會長這樣:
ruff-check...............................................................Passed
ruff-format..............................................................Passed
mypy.....................................................................Failed
- hook id: mypy
- exit code: 1
src/main.py:15: error: Incompatible return value type (got "None", expected "str")
Mypy 沒通過,commit 被擋住。你修好之後重新 git add 和 git commit 就行了。
實際開發的流程
# 1. 寫程式碼(或讓 AI 幫你寫)
# 2. git add
git add .
# 3. git commit — pre-commit 自動跑
git commit -m "feat: add user authentication"
# 情況 A:全部通過 ✅
# → commit 成功
# 情況 B:ruff format 自動修了格式 ⚠️
# → commit 失敗,但檔案已經被修好了
# → 重新 git add . && git commit 就好
# 情況 C:mypy 找到型別錯誤 ❌
# → commit 失敗,你需要手動修
# → 修好後重新 git add . && git commit
完整的 pyproject.toml 設定範例
# pyproject.toml
[project]
name = "my-project"
version = "0.1.0"
requires-python = ">=3.12"
# ── Ruff 設定 ──
[tool.ruff]
line-length = 80
target-version = "py312"
[tool.ruff.lint]
select = [
"E", # pycodestyle
"F", # pyflakes
"I", # isort
"N", # naming
"UP", # pyupgrade
"B", # bugbear
"S", # bandit(安全性)
"SIM", # simplify
]
[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101"] # 測試允許 assert
# ── Mypy 設定 ──
[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true
warn_unused_ignores = true
[[tool.mypy.overrides]]
module = "some_untyped_library.*"
ignore_missing_imports = true
常見問題
「我趕死線了,能不能跳過 hook?」
git commit --no-verify -m "hotfix: emergency patch"
--no-verify 會跳過所有 pre-commit hook。但這應該是非常罕見的例外,不是常態。如果你經常需要跳過,代表你的規則可能太嚴,或你需要花時間修掉累積的問題。
「Mypy 對第三方套件報錯怎麼辦?」
有些套件沒有提供型別資訊,mypy 會報 import 錯誤。兩種解法:
# 方法 1:安裝型別 stub
pip install types-requests types-redis types-PyYAML
# 方法 2:在 pyproject.toml 裡忽略特定套件
[[tool.mypy.overrides]]
module = "some_library.*"
ignore_missing_imports = true
「Ruff 跟 Black 要選哪個?」
2025 年的答案:選 Ruff。 它的 formatter 跟 Black 幾乎 100% 相容,但更快,而且你不需要額外裝一個工具。如果你的專案已經在用 Black,遷移到 Ruff 也很簡單。
「hook 跑太慢怎麼辦?」
Ruff 是 Rust 寫的,基本上不會慢。Mypy 可能會慢一點,尤其是第一次跑。可以用 mypy daemon 加速:
# 用 dmypy 做增量檢查,只檢查你改過的檔案
dmypy run -- src/
一句話總結
| 工具 | 做什麼 | 比喻 |
|---|---|---|
| Ruff | 風格、格式、常見錯誤 | 校稿編輯:錯字、格式、排版 |
| Mypy | 型別正確性 | 邏輯審查:你說的和你做的有沒有一致 |
| Pre-commit | 自動化執行 | 門禁系統:不通過就不讓你進門 |
三個搭在一起,你的程式碼品質就有了自動化的底線——不管是你寫的、同事寫的、還是 AI 寫的,都必須通過同樣的品質關卡才能進入 codebase。
Top comments (0)