為何開發一個 Python 型別檢查器這麼難?

在大型 Python 專案中,型別檢查器(Type Checker)能提早找出錯誤,也支援編輯器的型別提示。但它有時會對能正常執行的程式回報錯誤;專案變大後,檢查速度又可能拖慢 CI 或編輯器。要減少這些問題,光是讀懂型別標註還不夠。

過去十年間,Dropbox、微軟、Meta 與 Astral 四家公司先後成立全職專責團隊,合計投入數十位核心成員,持續開發與維護 Python 型別檢查器。

這些工具分別採用 Python、TypeScript、OCaml 與 Rust。它們面對的問題相似:既要理解 Python 靈活的動態行為,又要減少誤報,還得讓大型專案與編輯器都能快速得到結果。


本質困境:漸進式型別與動態語意

Python 允許開發者逐步加入型別標註:可以只標註一部分函式,其餘程式碼照舊執行。現代 Python 型別系統建立在 Siek 與 Taha 於 2006 年提出的「漸進式型別」(Gradual Typing)理論之上。對檢查器來說,它必須同時處理型別資訊完整與不完整的程式碼。

型別標註能幫檢查器找出不相容的操作,但未受完整檢查的程式碼仍可能在執行期發生型別錯誤。因此,通過檢查不代表執行時不會發生型別錯誤。「健全」(Sound)的型別系統會提供這樣的保證:程式通過檢查後,就不會發生該系統所防範的型別錯誤。這並不代表程式沒有其他 bug。

 Python 程式碼
   |
   +-- 型別資訊足夠
   |     |
   |     v
   |   檢查型別是否相容
   |
   +-- 型別資訊不足
         |
         v
       可能退回 Any
         |
         v
       可能漏掉型別錯誤

PEP 483 用「相容於」(is-consistent-with)判斷型別能否指派。它保留了子型別的規則:某個型別的值若能安全當成另一型別使用,就允許指派;另外也允許 Any 與任何型別互相指派。

但涉及 Any 時,這種相容關係沒有「遞移性」(Transitivity):A 能給 B、B 能給 C,不代表 A 就能給 C。

否則,只要 int 能傳給 Any、Any 又能傳給 str,系統就會推導出「int 可以當作 str 使用」的荒謬結論。PEP 483 直白地指出:

「本質上,Any 宣告了退回動態型別,並讓靜態檢查器閉嘴。」

PEP 484 更將檢查器的定位明文寫出:

「本質上,這類型別檢查器的運作方式就是一個非常強大的 Linter。」

對開發者而言,這種取捨會表現在兩種情況:能正常執行的程式被判定有錯誤,稱為「誤報」(False Positives);有型別問題的程式通過檢查,稱為「漏報」(False Negatives)。檢查器團隊必須持續調整規則,減少誤報,同時盡量找出有用的錯誤。

減少誤報,也要限制分析範圍

pyright 作者 Eric Traut 曾在官方討論區表示,型別安全並非檢查器的最高目標,許多設計決策都是在誤報與漏報之間取捨。

規格說明規則應如何運作;實際套用到數百萬行程式碼時,團隊還得處理各種邊界案例,並依使用者回饋修正誤報。mypy 在 GitHub 上累積了超過 2,700 個開放 Issue(截至 2026 年 10 月),其中的邊界案例與誤報回饋,也反映了這項維護工作。

Ori Roth 於 2022 年的研究《Python Type Hints are Turing Complete》證明,Python 的型別標註能用來模擬一般程式的計算過程,這種能力稱為「圖靈完備性」。因此,看似只是在比較兩個型別,實際上也可能需要處理和執行程式一樣複雜的計算。

這帶來了「不可判定性」(Undecidable):給定任意兩個合法型別,沒有一套通用演算法,能保證每次都在有限時間內算完,並正確判斷前者是否為後者的子型別。個別案例仍然可以解決,但無法保證所有案例都能算出答案;即使願意花更多時間逐步搜尋,也無法保證搜尋一定會結束。

對檢查器開發者來說,這意味著必須替分析設下界線。例如,泛型一層套一層,或比較型別時反覆回到同一個問題,工具就需要決定追查到哪裡為止。這樣才能避免檢查一直跑下去,代價是某些合法的型別寫法可能無法完整分析。

可變容器、裝飾器與動態屬性

除了理論限制,Python 在執行期可以改變物件與函式的行為,也讓靜態分析更難掌握型別:

學術研究《Python 3 Types in the Wild》(DLS 2020)指出,真實世界中重度使用動態特性的 Python 專案,往往會引發型別檢查器產生大量誤報。有些 Python 程式碼能正常執行,檢查器卻無法從原始程式碼與標註中取得足夠資訊,確認它的型別是否相容。


規格持續更新:十年間 17 份 PEP 的追趕

型別檢查器面對的規格持續演進。新功能可能改變工具解析程式碼、推導型別與重用分析結果的方式。

自 2014 年 PEP 484 提出以來,十年間社群陸續通過了超過 17 份型別相關 PEP:

年份PEP主要內容
2014483、484型別提示的理論與基礎規範
2016526變數標註
2017544、561Protocols、型別資訊的套件封裝
2019585、612泛型、ParamSpec
2021673、681Self、dataclass_transform
2022695、696型別語法、型別變數的預設值
2024742、747TypeIs、TypeForm

這些 PEP 擴充了型別語法,例如 Python 3.12 的 PEP 695 type 關鍵字,也牽動檢查器的內部設計。語法解析器(parser)要讀懂新寫法,泛型求解器要推導型別參數的值,快取格式也可能需要跟著調整。

此外,官方 typing spec 往往落後於實務需求。規格書自身坦承是由多份 PEP 規範「剪貼重組而成,不必然是連貫的整體」。

正因標準無法即時涵蓋所有需求,各大工具(如 mypy、pyright)不得不自行摸索實作,產生了大量未明文記載的「既成事實標準」(De facto standards)。為解決分歧,社群於 2023 年通過 PEP 729 成立 Typing Council 並推動一致性測試,但工具仍需逐項調整實作,才能減少彼此的差異。


生態系整合負擔:套件的型別資訊從哪裡來?

型別檢查器還得知道標準函式庫與數千個第三方套件提供哪些函式、接受哪些參數,以及回傳什麼。

typeshed 集中維護標準函式庫與部分第三方套件的型別宣告,是重要的資訊來源。第三方套件也可以自行附帶型別標註或存根檔案,並非全靠 typeshed 提供。

新型別檢查器需要支援 typeshed 中數千個 stub 檔案使用的型別語法,也得處理套件自行附帶的型別資訊。遇到缺少標註的套件時,還要決定如何繼續分析。除了核心演算法,套件相容性也是持續的維護工作。


效能與架構演進:四種語言的重寫史

型別檢查器要同時滿足兩種效能需求:CI 批次檢查可能需要掃描集中在同一儲存庫(Monorepo)的數百萬行程式碼,編輯器(IDE)則要求在數百毫秒內回應修改。

 Architecture of Major Python Type Checkers

 +----------------------------------------------+
 | mypy (Python + C / mypyc)                    |
 | - Whole-program batch analysis               |
 +----------------------------------------------+

 +----------------------------------------------+
 | pyright (TypeScript / Node.js)               |
 | - Lazy evaluation for responsive IDE         |
 +----------------------------------------------+

 +----------------------------------------------+
 | Pyrefly / ty (Rust)                          |
 | - Multi-threaded analysis                    |
 +----------------------------------------------+

為了同時應付大型專案與編輯器的回應速度,各團隊採取了不同架構,也有人選擇重新實作:

  1. mypy(Python + C / mypyc):早期採用單檔依序處理架構,隨著專案膨脹面臨嚴峻效能瓶頸。團隊後續引入了增量快取以重用先前的分析結果,並讓守護行程(dmypy)常駐,減少重複啟動成本。團隊還耗費數月開發專屬編譯器 mypyc 將 Python 程式碼編譯為 C 擴充模組,換取約 4 倍加速。
  2. pyright(TypeScript / Node.js):微軟於 2019 年發布 pyright,主打比 mypy 快 3 至 5 倍。它專為向編輯器提供型別資訊的 Language Server 設計,採用延遲求值(Lazy / Just-In-Time Evaluation),等需要某項結果時才計算,語法解析器則容許程式碼尚未寫完,仍能繼續分析。pyright 隨後成為 VS Code 官方 Python 擴充套件 Pylance 的核心引擎。
  3. Pyre → Pyrefly(OCaml → Rust):Meta 為 Instagram 的巨型程式碼庫開發出以 OCaml 實作的 Pyre;2025 年,Meta 宣布由 15 人團隊以 Rust 完全重寫為 Pyrefly,專注於現代 IDE 的即時回應。
  4. ty(Rust):Astral 團隊繼 Ruff 與 uv 之後,以 10 人團隊推出全新檢查器 ty。第三方實測顯示其檢查速度比 mypy 快 9 至 21 倍。Astral 坦言 ty 是團隊開發過最複雜的產品,必須同時精通型別理論、執行期語意與真實生態。

人力投入的真實代價

前面的工作都需要持續投入人力:修正誤報、支援新規格、追蹤套件 API,並改善大型專案的檢查速度。

Jukka Lehtosalo 在回顧 mypy 的演進時曾感慨:

「早期做出的架構決策,會在多年後持續帶來痛苦。」

Dropbox 自 2015 年底起維持全職核心團隊,持續開發 mypy,並在內部數百萬行 Python 程式碼中反覆驗證與改進。

下面整理各專案的核心語言,以及已公開的團隊規模與投入歷程:

專案主要推手核心語言團隊規模與投入歷程
mypyDropboxPython (mypyc / C)專責團隊 9+ 人,持續全職資助逾 10 年
pyright微軟TypeScript核心作者 Eric Traut 主導,成為 Pylance 核心
PyreflyMetaRust專責團隊 15 人,接替 OCaml 版 Pyre
tyAstralRust核心團隊 10 人,專注於 IDE 即時效能突破

Python 型別檢查器必須同時處理多種限制:程式碼可能缺少標註,物件可能在執行期改變,規格與套件 API 持續更新,而大型專案和編輯器又都要求快速回應。團隊得在分析範圍、誤報率與效能之間取捨,才能提供IDE 即時回饋。

開發者看到的一則誤報或一次檢查延遲,背後可能牽涉型別規則、資料流分析、套件存根或快取設計。四家公司十年間持續投入核心團隊,就是為了讓這些限制在日常開發中更少造成阻礙。