Python Type Hints 完全教學:從基礎到 mypy 靜態型別檢查實戰
為什麼需要 Type Hints
寫 Python 寫久了,一定遇過這種情況:一個函式收到的參數到底是 str 還是 list?三個月前自己寫的程式碼,現在完全看不懂參數該傳什麼。Type Hints 就是來解決這個痛點的。
加上型別提示有四大好處:
- 提早抓 Bug:搭配 mypy 等工具,在程式執行前就能發現型別錯誤,不用等到 runtime 才爆炸。
- IDE 支援升級:VS Code、PyCharm 等編輯器能根據型別提供更精準的自動補全和錯誤提示。
- 活文件:型別標註就是最好的文件,比寫 docstring 更不容易過期。
- 重構更安全:改動函式簽章時,工具會立刻告訴你哪些呼叫端需要跟著改。
值得一提的是,Type Hints 是 Python 3.5 以後才正式引入的,而且它是漸進式的——你不需要一次把整個專案都加上型別,可以從最關鍵的模組開始。這點跟 TypeScript 的理念很像。
基礎語法
最基本的用法就是對變數和函式加上型別標註:
# 變數型別標註
name: str = "Alice"
age: int = 30
is_active: bool = True
score: float = 95.5
# 函式型別標註
def greet(name: str) -> str:
return f"你好,{name}!"
# 沒有回傳值用 None
def log_message(msg: str) -> None:
print(f"[LOG] {msg}")
語法很直覺:變數名後面加冒號和型別,函式回傳值用 -> 箭頭標註。這些標註在 runtime 不會有任何影響,Python 直譯器會完全忽略它們。
常用型別速查
容器型別
Python 3.9 以後可以直接用小寫的內建型別當泛型,不用再從 typing 匯入了:
# Python 3.9+ 寫法(推薦)
names: list[str] = ["Alice", "Bob"]
ages: dict[str, int] = {"Alice": 30, "Bob": 25}
coordinates: tuple[float, float] = (25.03, 121.56)
unique_ids: set[int] = {1, 2, 3}
# 巢狀容器也沒問題
matrix: list[list[int]] = [[1, 2], [3, 4]]
config: dict[str, list[str]] = {"admins": ["alice", "bob"]}
Optional 與 Union
當一個值可能是某個型別或 None 時,用 Optional。Python 3.10 以後可以用更簡潔的 | 語法:
# Python 3.10+ 用 | 語法
def find_user(user_id: int) -> dict | None:
"""找不到使用者時回傳 None"""
...
# 多型別聯合
def process(value: str | int | float) -> str:
return str(value)
# Python 3.9 以前的寫法
from typing import Optional, Union
def find_user_old(user_id: int) -> Optional[dict]:
...
進階型別
TypeVar 與 Generics
當你寫一個函式要處理「任意型別但要保持一致」的情況,就需要泛型。Python 3.12 引入了全新的語法,寫起來超級乾淨:
# Python 3.12+ 新語法
def first[T](items: list[T]) -> T:
return items[0]
# 泛型 class
class Stack[T]:
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
# 使用時型別會自動推斷
stack = Stack[int]()
stack.push(42)
value: int = stack.pop() # mypy 知道這是 int
Protocol 結構化型別
Protocol 是 Python 版的「鴨子型別」正式化。不需要繼承,只要物件有對應的方法就算符合:
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
class Circle:
def draw(self) -> None:
print("畫圓形")
class Square:
def draw(self) -> None:
print("畫方形")
def render(shape: Drawable) -> None:
shape.draw()
# Circle 和 Square 都沒有繼承 Drawable,但因為有 draw 方法就能用
render(Circle()) # OK
render(Square()) # OK
這在寫 plugin 架構或依賴注入時特別好用,如果你對設計模式有興趣,可以參考 Python 裝飾器教學 了解更多進階用法。
TypedDict
對於那些用 dict 當作結構體在傳的場景,TypedDict 可以幫你精確定義每個 key 的型別:
from typing import TypedDict
class UserProfile(TypedDict):
name: str
age: int
email: str
is_premium: bool
def get_display_name(user: UserProfile) -> str:
prefix = "⭐ " if user["is_premium"] else ""
return f"{prefix}{user['name']}"
user: UserProfile = {
"name": "Alice",
"age": 30,
"email": "[email protected]",
"is_premium": True
}
mypy 安裝與設定
mypy 是 Python 生態系最成熟的靜態型別檢查工具,安裝很簡單:
pip install mypy
# 檢查單一檔案
mypy app.py
# 檢查整個專案
mypy src/
mypy.ini 設定建議
在專案根目錄放一個 mypy.ini,統一團隊的檢查標準:
[mypy]
python_version = 3.12
warn_return_any = True
warn_unused_configs = True
disallow_untyped_defs = True
check_untyped_defs = True
no_implicit_optional = True
# 第三方套件若沒有型別 stub,先忽略
[mypy-requests.*]
ignore_missing_imports = True
[mypy-celery.*]
ignore_missing_imports = True
mypy strict mode
如果你想用最嚴格的標準來檢查,可以開啟 strict mode:
mypy --strict src/
strict mode 會開啟所有嚴格檢查,包括禁止 Any 型別、要求所有函式都有型別標註等。建議新專案從一開始就用 strict mode,舊專案則慢慢遷移。
新手常犯的 3 個錯誤
錯誤一:忘記標註 None 回傳
# ❌ 有時回傳 None 但沒標註
def find_item(name: str) -> dict:
if name in cache:
return cache[name]
# 隱式回傳 None,但型別說回傳 dict!
# ✅ 正確寫法
def find_item(name: str) -> dict | None:
if name in cache:
return cache[name]
return None
錯誤二:用具體型別而非抽象型別
from collections.abc import Iterable, Mapping
# ❌ 參數型別太具體,限制了呼叫端
def process_items(items: list[str]) -> None:
for item in items:
print(item)
# ✅ 用 Iterable 更彈性,tuple、set、generator 都能傳
def process_items(items: Iterable[str]) -> None:
for item in items:
print(item)
錯誤三:濫用 Any
from typing import Any
# ❌ Any 等於關閉型別檢查
def process(data: Any) -> Any:
return data["key"]
# ✅ 盡量用具體型別或泛型
def process(data: dict[str, str]) -> str:
return data["key"]
如果你在 debug 時也想要更好的型別追蹤,搭配 Python logging 模組進階設定 可以建立更完善的錯誤追蹤機制。
漸進式導入策略
如果你手上有一個幾萬行的舊專案,千萬不要想著一次全加型別,那樣只會讓你崩潰。建議的步驟:
- 從核心模組開始:先為最常被呼叫的模組加型別,投資報酬率最高。
- 新程式碼必須有型別:設立團隊規範,新寫的程式碼都要通過 mypy 檢查。
- per-module 設定:在
mypy.ini中可以對不同模組設定不同的嚴格程度,已加型別的模組用 strict,還沒加的先跳過。 - 整合 CI:把 mypy 加到 CI pipeline,確保每次 PR 都不會引入新的型別錯誤。
# GitHub Actions 範例
- name: Type Check
run: |
pip install mypy
mypy src/ --config-file mypy.ini
這個策略的關鍵是「不要追求完美」,先求有再求好。根據我的經驗,大概花兩到三個 sprint 就能把核心模組的型別覆蓋率拉到 80% 以上。
常見問題
Q:Type Hints 會影響執行效能嗎?
不會。Python 直譯器在 runtime 完全忽略型別標註,不會有任何效能損失。唯一的「成本」是開發者多打一些字。
Q:除了 mypy 還有其他工具嗎?
有的。Pyright(微軟開發,VS Code 的 Pylance 底層引擎)速度比 mypy 快很多,而且直接內建在 VS Code 裡。Pyre(Meta 開發)也是選項之一。三者的型別系統大同小異,擇一使用即可。
Q:是不是所有程式碼都要加型別?
不需要。測試程式碼、一次性腳本通常不需要費心加型別。重點放在:公開 API、跨模組介面、複雜的資料處理邏輯。這些地方的型別標註投報率最高。
繼續閱讀
Python 裝飾器 Decorator 從入門到進階:用實例搞懂這個神奇語法
Python 裝飾器看起來很神奇,但其實原理不難。從基礎語法到進階用法,帶你用實例徹底搞懂 Decorator。
相關文章
你可能也喜歡
探索其他領域的精選好文