將單一檔案遷移至使用更嚴格的 Pyrefly 型別檢查,要求所有函式、類別與屬性皆須加上型別標註。
Pyrefly 型別覆蓋率 Skill
前置條件
- 該檔案必須位於包含
pyrefly.toml的專案中。 pyrefly、lintrunner以及專案的測試執行工具(test runner)必須已加入 PATH。若缺少其中任何一項,請停止執行並詢問是否需要啟用 conda 環境 — 切勿自行安裝或替換(依據專案 repo 的 CLAUDE.md 規範)。
步驟 1:移除檔案層級的型別檢查抑制(suppression)
從檔案頂部刪除以下任何註解(pyrefly 為了相容 mypy 會採納 # mypy: ignore-errors,因此該項也必須移除):
# pyre-ignore-all-errors
# pyre-ignore-all-errors[16,21,53,56]
# @lint-ignore-every PYRELINT
# mypy: ignore-errors
步驟 2:在 pyrefly.toml 中新增子設定區段(sub-config)
[[sub-config]]
matches = "path/to/directory/**"
[sub-config.errors]
implicit-import = false
implicit-any = true
bad-param-name-override = false
unannotated-return = true
unannotated-parameter = true
重要事項:在 [sub-config.errors] 中設定任何錯誤鍵值(error key),只會相對於父層設定覆寫該鍵值 — 但若啟用 unannotated-return / unannotated-parameter / implicit-any,原本在全檔範圍被隱藏的錯誤將會重新浮現。若您看到無關的錯誤(例如 bad-param-name-override)大量洗版輸出,請在子設定中同步父層設定對該鍵值的設定值以消音。
步驟 3:執行 pyrefly
pyrefly check <FILENAME>
目標: 透過新增型別標註來解決所有 unannotated-return、unannotated-parameter 與 implicit-any 錯誤 — 詳情請參閱步驟 4 的階梯原則。這三個目標類別總是能夠被解決的;絕不要使用 # pyrefly: ignore 來抑制它們。唯一的例外是 @compatibility(is_backward_compatible=True)(步驟 4)。
其他類別(bad-argument-type、missing-attribute 等)屬於真實的型別 Bug。請根據 pyrefly 報告錯誤的位置來處置:
- 於其他檔案回報(路徑 != 目標檔案):保持原樣,勿擴大變更範圍。若該錯誤因而阻礙了目標檔案的處理,請在回報出處使用
# pyrefly: ignore[<category>] # TODO進行抑制。 - 於目標檔案回報,但訊息指向別處定義的符號(例如因匯入函式的標註有誤而導致
bad-return):在本地使用相同的 TODO 註解予以抑制。切勿憑空加上cast()來掩蓋上游缺口。 - 於目標檔案回報,且起源於本地:直接修復。
僅將 # pyrefly: ignore[...] 作為最後手段,且僅限用於非目標類別。
步驟 4:新增型別標註
當無法直接從函式主體推斷出正確型別時,請檢查呼叫處(call sites)。
型別標註慣例
- 使用 PEP 604 / PEP 585 語法(
int | None、list[str]) — 假設 Python >= 3.10。 - 對於抽象基底類別(ABCs,如
Callable、Sequence、Generator...),優先使用collections.abc而非typing。 - 對於泛型輔助工具(generic helpers),只要專案支援的最低 Python 版本有提供,就從
typing匯入;僅在需要較新功能時才從typing_extensions匯入(例如支援 < 3.11/3.12 時所需的Self與override,或 PEP 696 中TypeVar/ParamSpec的default=)。請勿一律盲目從typing_extensions匯入。 - 務必為
Callable加上參數型別(絕不要使用裸露的Callable)。優先考慮Callable[..., object];只有在呼叫方確實會使用動態回傳值時,才使用Callable[..., Any]— 若回傳結果只是被原封不動傳遞(或該可呼叫物件甚至未被呼叫),使用object更為嚴謹且同樣正確。(若為需要保留簽章的包覆器(wrapper)情境,請參閱下方的 ParamSpec 部分。) - 為您所新增的任何模組區域全域變數開頭加上底線 — 包括
TypeVar/ParamSpec(需與字串引數一致:_T = TypeVar("_T")、_P = ParamSpec("_P")、_R = TypeVar("_R"))、TypeAlias、輔助常數以及 Sentinel。這是 torch 處理非公開名稱的主流慣例(在程式碼樹中_P的數量約為P的 6 倍)。例外情況(不加前綴底線):會被其他模組匯入的名稱、已列於__all__中、或用作執行期 Token 者(例如標註字串派發標記)。此規則僅適用於您新增的名稱 — 切勿重新命名已存在的全域變數,那是超出本 Skill 範疇的無關重構。 - 布林判斷式(boolean predicate) — 命名為
is_*/has_*、接收寬鬆型別(通常為object)並回傳bool— 通常適合使用TypeGuard[X](或TypeIs[X],後者還能窄化否定分支)。TypeGuard已收錄於typing(>= 3.10,故直接自該處匯入);TypeIs則是在 3.13 才加入typing,因此需自typing_extensions(>= 4.10)匯入以維持對 3.10 的相容性。接收klass: type[_T]的issubclass風格輔助函式應回傳TypeGuard[type[_T]]。相較於用try/except TypeError包裹issubclass(),優先使用顯式的isinstance(x, type)檢查防護 — 這樣更清晰,且能讓型別檢查器進行窄化推導。 - 當回傳型別是衍生自某個參數時 — 例如原樣傳遞/恆等函式(passthrough/identity functions)、「回傳傳入引數之一」的輔助函式、裝飾器(decorators)、以型別為 Key 的註冊表(registries) — 請使用
TypeVar(或者針對簽章需要傳遞的可呼叫引數,搭配ParamSpec/TypeVar使用Callable[_P, _R]),而非放寬為object/Any。「輸出型別 == 某個輸入型別」正是TypeVar所表達的核心含義;使用object輸入 /object輸出會丟失此資訊。注意事項:若函式會轉化該值以致輸出型別與輸入不同(例如將陣列轉換為整數),使用單一TypeVar是錯誤的 — 此時應直接指定實際的領域型別(domain type)。 - 在
__init__中指派的類別屬性應加上類別層級的型別標註,以便 pyrefly 能夠識別。 - 使用
if TYPE_CHECKING:來打破循環匯入(import cycles) — 僅用於標註的匯入應放在檢查條件內,並使用from __future__ import annotations(或字串形式的前向引用 forward refs)以保持執行期匯入的延遲載入特性:from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from torch.fx import GraphModule def transform(gm: GraphModule) -> GraphModule: ... - 絕不要抑制那三個目標類別。
unannotated-return、unannotated-parameter與implicit-any總是能透過新增標註來解決;使用# pyrefly: ignore[<其中之一>]是無法接受的結果。唯一的例外是下方的「向下相容例外條款」。 - 放寬型別,而非放棄。 當難以推斷正確型別時,請依據以下階梯原則逐級向下嘗試,而不是直接加上 ignore 註解:
- 從呼叫處與回傳路徑中可觀察到的最具體型別(concrete type)。
- 聯集型別(
X | Y)、Sequence[X]風格的抽象型別,或是針對真正通用泛型函式(原樣傳遞、容器輔助工具)的受限TypeVar(boundTypeVar)。 object— 能通過型別檢查的最嚴格退路。強迫呼叫方在使用前必須先窄化型別,例如def serialize(value: object) -> str:。視覺上與Any相似但更為嚴格 — pyrefly 在沒有isinstance檢查的情況下會拒絕value.foo()。Any— 最底層階梯。在目標類別上總是優先於使用# pyrefly: ignore,但前提是第 1–3 階梯皆已嘗試失敗。必須能夠清楚說明為何先前各階梯皆不適用(例如:「聯集超過 8 種型別」、「無法觀察到共同邊界」、「呼叫方確實從不窄化」)。
- 在回傳位置使用
object/Any時要特別謹慎 — 函式自身通常比其呼叫方更清楚自己產出的內容。只有在真正的邊界處(例如原封不動回傳輸入值,或該值是由處理常式/呼叫方所定義),寬鬆的回傳型別才是正確的;若函式主體建構的是已知結構,請明確指定名稱(使用領域別名或聯集型別皆勝過object)。 - 在決定將參數設為
Any之前,請至少閱讀三個呼叫處 — 切勿在第一次嘗試時僅憑「看起來很動態」就做出判斷。 - 窄範圍的
# pyrefly: ignore[...](用於非目標類別)僅保留給 pyrefly 對特定局部錯誤確實判斷錯誤的情況 — 例如動態元程式設計(dynamic metaprogramming)、第三方存根檔(stub)缺失等:# pyrefly: ignore[attr-defined] result = getattr(obj, dynamic_name)() - 當單行內的
# pyrefly: ignore[...]會導致行長超過限制時,請將其放置於被標記行的正上方一行,而非使用# fmt: skip強行留在同行 — pyrefly 能識別上一行的 ignore 註解。(例外:下方的向下相容例外條款,其必須位於def行)。
向下相容性(絕不抑制規則的唯一例外)
關鍵重要:標有 @compatibility(is_backward_compatible=True) 裝飾器的函式絕不可變更其函數簽章(signature)。向下相容性測試(test_function_back_compat)會將轉為字串的 inspect.signature 與 Baseline 檔(golden file)進行比對 — 新增標註(即使是 -> None)都會改變該字串並導致測試失敗。此時請改用 pyrefly ignore 註解:
@compatibility(is_backward_compatible=True)
def my_function( # pyrefly: ignore[unannotated-return]
self,
arg1, # can't add type here either
):
...
# pyrefly: ignore 註解必須位於 def 那一行(即 pyrefly 報告錯誤的位置),而非位於結尾的 ) 行。
用於保留簽章之包覆器的 ParamSpec(裝飾器、functools.wraps 風格的輔助函式)。請使用 Callable[P, R],使被包裹函式的簽章能透傳給呼叫方 — 若使用 Callable[..., Any] 則會丟失簽章資訊。若該包覆器確實可接收任意 Callables,則可跳過 ParamSpec。當包覆器會在參數最前或最後附加引數時,請搭配 Concatenate[X, P] 使用。
from collections.abc import Callable
from typing import ParamSpec, TypeVar
_P = ParamSpec("_P")
_R = TypeVar("_R")
def log_calls(fn: Callable[_P, _R]) -> Callable[_P, _R]:
def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
return fn(*args, **kwargs)
return wrapper
步驟 5:反覆迭代
重新執行 pyrefly check。新增的型別標註往往會浮現出 bad-return 錯誤(即函式實際上回傳了不相容的型別) — 請修復這些錯誤。重複此過程直到檢查完全通過(clean)。
限縮或嚴格化共享輔助函式(例如新增 TypeGuard 或更精準的回傳型別)可能會導致其呼叫方原本存在的 # pyrefly: ignore 註解失效。請重新檢查並刪除已無用的抑制註解與過時的說明文字 — 切勿保留。
步驟 6:程式碼檢查(Lint)
在交卷前為必要步驟 — 新增型別標註經常會改變匯入順序與行長度:
lintrunner -a <files...>
若有 lintrunner 無法自動修復的項目,請手動予以解決。
步驟 7:測試
發生失敗時的優先順序:測試通過 > pyrefly 檢查乾淨 > 型別標註嚴謹度。若新加入的型別標註導致測試失敗,請在還原檔案前,依據階梯原則退回一個階梯(例如從具體型別退至 object,或移除破壞了下游 isinstance 檢查的 Any 放寬)。
-
向下相容性檢查。 當且僅當
grep -l '@compatibility(is_backward_compatible=True)' <target>回傳該檔案時才執行 — 該裝飾器才是 Baseline 檔的真正前置條件。較為寬鬆的「匯入torch.fx」啟發式檢查會誤涵蓋半數的torch/檔案。python -m pytest test/test_fx.py::TestFXAPIBackwardCompatibility -x -v -
受修改模組的單元測試。 在判定「不存在測試覆蓋」之前,請雙向搜尋比對:
# torch/foo/bar.py is usually covered by test/test_foo.py or test/test_bar.py ls test/ | grep -i <module-name> # or by import grep -rl "from torch.foo.bar import\|import torch.foo.bar" test/若兩者皆查無結果,請告知使用者 — 切勿默默跳過。型別變更有可能引入真實的執行期退化(runtime regressions,例如
Optional[X]與X的差異,或當呼叫.append時Sequence與list的差異等)。
補充說明
- 類別主體中的前向引用(Forward refs):若未加上
from __future__ import annotations,仍需使用字串引號包裹:class MyClass: def __new__(cls) -> "MyClass": ... - 提交(Committing):除非使用者明確要求,否則請勿 commit(依據專案 repo 的 CLAUDE.md 規範)。當檔案通過檢查後,請停止操作並輸出 diff 供審閱。






