DEV Community

Theo Hsiung
Theo Hsiung

Posted on

給 Junior 的 Python 品質工具入門:Ruff 與 Mypy

你的程式碼能跑不代表它沒問題。這兩個工具幫你在「出事之前」就抓到問題。


問題的起點:Python 太自由了

Python 是一個非常「寬容」的語言。它讓你做很多事情,但不會告訴你做錯了:

def add(a: int, b: int) -> int:
    return a + b

result = add("hello", "world")  # Python 不會報錯,直接跑
print(result)                    # "helloworld" — 你說要 int,它收到 str,沒人管
Enter fullscreen mode Exit fullscreen mode
import json        # 你 import 了但沒用到,Python 不管
import os          # 這個也沒用到,Python 也不管

def process(data ,count):    # 逗號前面多了空格,Python 不管
    temp = 42                 # 定義了變數但沒用,Python 不管
    result = data*count
    return result
Enter fullscreen mode Exit fullscreen mode

這些問題不會讓程式「爆掉」,但會慢慢腐蝕你的 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'
                                   # 半夜三點,上線才爆
Enter fullscreen mode Exit fullscreen mode

Python 不會警告你,直到程式跑到那一行才炸掉。

有 Mypy 的世界

$ mypy your_code.py
your_code.py:3: error: Incompatible return value type (got "None", expected "str")
Enter fullscreen mode Exit fullscreen mode

程式還沒跑就告訴你哪裡錯了。 你可以這樣修:

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())
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

怎麼用

# 安裝
pip install mypy

# 檢查單一檔案
mypy your_code.py

# 檢查整個專案
mypy src/

# 嚴格模式(所有函式都必須有 type annotation)
mypy --strict src/
Enter fullscreen mode Exit fullscreen mode

設定(在 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
$ 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.
Enter fullscreen mode Exit fullscreen mode

自動修復能修的:

$ ruff check --fix bad_code.py
Found 4 errors (2 fixed, 2 remaining).
# 自動移除沒用到的 import、修正空格
# class 命名和未使用變數需要你自己改(因為 ruff 不確定你的意圖)
Enter fullscreen mode Exit fullscreen mode

Ruff 當 Formatter:幫你統一風格

# 格式化前
def   foo( a,b,   c ):
    return   {"key":a,"key2":    b,"key3":c}

x=[1,2,
 3,4,5]
Enter fullscreen mode Exit fullscreen mode
$ ruff format bad_code.py
1 file reformatted
Enter fullscreen mode Exit fullscreen mode
# 格式化後
def foo(a, b, c):
    return {"key": a, "key2": b, "key3": c}

x = [1, 2, 3, 4, 5]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

怎麼用

# 安裝
pip install ruff

# Lint:找出問題
ruff check .

# Lint + 自動修復
ruff check --fix .

# Format:自動統一風格
ruff format .

# 查看會改什麼(不實際修改)
ruff format --check .
Enter fullscreen mode Exit fullscreen mode

設定(在 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
Enter fullscreen mode Exit fullscreen mode

Ruff vs Mypy:各管什麼?

這兩個工具是互補的,不是替代關係:

               Ruff                          Mypy
        ┌──────────────────┐          ┌──────────────────┐
        │  風格和格式       │          │  型別正確性       │
        │  ─────────────── │          │  ─────────────── │
        │  ✓ import 排序    │          │  ✓ 型別不匹配     │
        │  ✓ 未使用的變數   │          │  ✓ 忘記處理 None  │
        │  ✓ 命名規範       │          │  ✓ 回傳型別錯誤   │
        │  ✓ 空格/縮排      │          │  ✓ 屬性不存在     │
        │  ✓ 安全性問題     │          │  ✓ 參數型別錯誤   │
        │  ✓ 過時語法       │          │                  │
        └──────────────────┘          └──────────────────┘
               看得整不整齊                  用得對不對
Enter fullscreen mode Exit fullscreen mode

一個實際的例子:

import json                        # ← Ruff 會抓(F401: 未使用的 import)

def add(a: int, b: int) -> int:
    return a + b

result = add("hello", "world")     # ← Mypy 會抓(型別不匹配)
temp = 42                           # ← Ruff 會抓(F841: 未使用的變數)
Enter fullscreen mode Exit fullscreen mode

兩個都用,才是完整的品質檢查。


整合 Git Pre-commit Hook

為什麼需要?

你可以手動跑 ruff checkmypy,但你會忘。特別是趕進度的時候。

Pre-commit hook 的意思是:每次你執行 git commit,自動先跑這些檢查。沒通過就不讓你 commit。

git commit
    │
    ├── ① ruff check(lint)     → 有未使用的 import?❌ 擋住
    ├── ② ruff format(格式化)   → 格式不對?自動修好,要你重新 commit
    └── ③ mypy(型別檢查)        → 型別錯誤?❌ 擋住
    │
    ✅ 全部通過 → commit 成功
Enter fullscreen mode Exit fullscreen mode

這樣不管你多趕,品質都有底線。

Step 1:安裝 pre-commit

pip install pre-commit
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

注意順序:ruff-check(帶 --fix)要在 ruff-format 前面。 因為 lint 修復可能產生需要再格式化的程式碼。

Step 3:啟用 hook

# 安裝 hook 到 .git/hooks/
pre-commit install

# 完成!之後每次 git commit 都會自動跑
Enter fullscreen mode Exit fullscreen mode

Step 4:試跑看看

# 手動對所有檔案跑一次(建議第一次設定時執行)
pre-commit run --all-files
Enter fullscreen mode Exit fullscreen mode

輸出會長這樣:

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")
Enter fullscreen mode Exit fullscreen mode

Mypy 沒通過,commit 被擋住。你修好之後重新 git addgit 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
Enter fullscreen mode Exit fullscreen mode

完整的 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
Enter fullscreen mode Exit fullscreen mode

常見問題

「我趕死線了,能不能跳過 hook?」

git commit --no-verify -m "hotfix: emergency patch"
Enter fullscreen mode Exit fullscreen mode

--no-verify 會跳過所有 pre-commit hook。但這應該是非常罕見的例外,不是常態。如果你經常需要跳過,代表你的規則可能太嚴,或你需要花時間修掉累積的問題。

「Mypy 對第三方套件報錯怎麼辦?」

有些套件沒有提供型別資訊,mypy 會報 import 錯誤。兩種解法:

# 方法 1:安裝型別 stub
pip install types-requests types-redis types-PyYAML

# 方法 2:在 pyproject.toml 裡忽略特定套件
Enter fullscreen mode Exit fullscreen mode
[[tool.mypy.overrides]]
module = "some_library.*"
ignore_missing_imports = true
Enter fullscreen mode Exit fullscreen mode

「Ruff 跟 Black 要選哪個?」

2025 年的答案:選 Ruff。 它的 formatter 跟 Black 幾乎 100% 相容,但更快,而且你不需要額外裝一個工具。如果你的專案已經在用 Black,遷移到 Ruff 也很簡單。

「hook 跑太慢怎麼辦?」

Ruff 是 Rust 寫的,基本上不會慢。Mypy 可能會慢一點,尤其是第一次跑。可以用 mypy daemon 加速:

# 用 dmypy 做增量檢查,只檢查你改過的檔案
dmypy run -- src/
Enter fullscreen mode Exit fullscreen mode

一句話總結

工具 做什麼 比喻
Ruff 風格、格式、常見錯誤 校稿編輯:錯字、格式、排版
Mypy 型別正確性 邏輯審查:你說的和你做的有沒有一致
Pre-commit 自動化執行 門禁系統:不通過就不讓你進門

三個搭在一起,你的程式碼品質就有了自動化的底線——不管是你寫的、同事寫的、還是 AI 寫的,都必須通過同樣的品質關卡才能進入 codebase。

Top comments (0)