pyrefly-type-coverage

Migrate a file to use stricter Pyrefly type checking with annotations required for all functions, classes, and attributes.

INSTALLATION
npx skills add https://github.com/pytorch/pytorch --skill pyrefly-type-coverage
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$27

Step 2: Add a sub-config entry to pyrefly.toml

[[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

IMPORTANT: Setting any error key in [sub-config.errors] overrides only that key

relative to the parent — but enabling unannotated-return / unannotated-parameter /

implicit-any will resurface errors that were previously hidden file-wide. If you see

unrelated errors (e.g., bad-param-name-override) flooding the output, mirror the

parent config's setting for that key in the sub-config to silence them.

Step 3: Run pyrefly

pyrefly check <FILENAME>

Goal: resolve all unannotated-return, unannotated-parameter, and implicit-any

errors by adding annotations — see Step 4's ladder. These three target categories are

always resolvable; never suppress them with # pyrefly: ignore. The single

exception is @compatibility(is_backward_compatible=True) (Step 4).

Other categories (bad-argument-type, missing-attribute, …) are real type bugs.

Handle them by where pyrefly reports them:

  • Reported in another file (path != target): leave it. Don't widen scope. If

the error is now blocking the target, suppress at the report site with

# pyrefly: ignore[<category>] # TODO.

  • Reported in the target file but the message names a symbol defined elsewhere

(e.g., bad-return because an imported function's annotation is wrong):

suppress locally with the same TODO comment. Don't invent a cast() that

papers over the upstream gap.

  • Reported in the target file, originates locally: fix it.

Use # pyrefly: ignore[...] only as a last resort, and only on non-target categories.

Step 4: Add annotations

Examine call sites when the right type isn't obvious from the function body.

#### Annotation conventions

  • Use PEP 604 / PEP 585 syntax (int | None, list[str]) — assume Python >= 3.10.
  • Prefer collections.abc over typing for ABCs (Callable, Sequence, Generator, ...).
  • For generic helpers, import from typing when available on the project's minimum

Python version, and from typing_extensions only when you need a newer feature

(e.g., Self and override if supporting < 3.11/3.12, or PEP 696 default= for

TypeVar / ParamSpec). Don't blanket-import from typing_extensions.

  • Always parameterize CallableCallable[..., Any] when the signature is

genuinely unknown, never bare Callable. (See ParamSpec below for the

signature-preserving wrapper case.)

  • Class attributes assigned in __init__ should get a class-level annotation so pyrefly can see them.
  • Break import cycles with if TYPE_CHECKING: — annotation-only imports go inside the

guard, and use from __future__ import annotations (or string forward refs) so

runtime imports stay lazy:

from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:

    from torch.fx import GraphModule

def transform(gm: GraphModule) -> GraphModule: ...
  • Never suppress the three target categories. unannotated-return,

unannotated-parameter, and implicit-any are always resolvable by adding

an annotation; # pyrefly: ignore[<one of those>] is not an acceptable

outcome. The single exception is the Backward compatibility carve-out below.

  • Widen, don't bail. When the right type is hard to infer, walk down this

ladder rather than reaching for an ignore:

  • Most specific concrete type observable from call sites and return paths.
  • A union (X | Y), Sequence[X]-style abstract type, or a bound TypeVar

for genuinely generic functions (identity-passthrough, container helpers).

  • object — strictest fallback that still type-checks. Forces callers to

narrow before use, e.g., def serialize(value: object) -> str:. Visually

similar to Any but stricter — pyrefly rejects value.foo() without an

isinstance.

  • Any — last rung. Always preferred over a # pyrefly: ignore on a target

category, but only after rungs 1–3 fail. Be able to articulate why each

earlier rung doesn't fit (e.g., "union exceeds 8 types", "no observable

common bound", "callers genuinely never narrow").

  • Read at least three call sites before deciding a parameter must be Any

don't pattern-match "looks dynamic" on the first try.

  • Narrow-scope # pyrefly: ignore[...] (on a non-target category) is reserved

for cases where pyrefly is actually wrong about a specific local error —

dynamic metaprogramming, third-party stub gaps:

# pyrefly: ignore[attr-defined]

result = getattr(obj, dynamic_name)()

#### Backward compatibility (the one exception to never-suppress)

CRITICAL: Functions decorated with @compatibility(is_backward_compatible=True)

must NOT have their signatures changed. The backward-compat test

(test_function_back_compat) compares stringified inspect.signature against a golden

file — adding annotations (even -> None) changes that string and the test fails.

Use pyrefly ignore comments instead:

@compatibility(is_backward_compatible=True)

def my_function(  # pyrefly: ignore[unannotated-return]

    self,

    arg1,  # can't add type here either

):

    ...

The # pyrefly: ignore comment must be on the def line (where pyrefly reports the error),

not on the closing ).

ParamSpec for signature-preserving wrappers (decorators, functools.wraps-style

helpers). Use Callable[P, R] so the wrapped function's signature flows through

to the caller — Callable[..., Any] loses it. Skip ParamSpec if the wrapper

genuinely accepts arbitrary callables. Pair with Concatenate[X, P] when the

wrapper prepends or appends args.

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

Step 5: Iterate

Re-run pyrefly check. New annotations often surface bad-return errors where the

function actually returns an incompatible type — fix those. Repeat until clean.

Step 6: Lint

Required before handing off — annotations frequently shift import order and line

length:

lintrunner -a <files...>

Resolve anything lintrunner can't auto-fix manually.

Step 7: Test

Precedence when something fails: tests passing > pyrefly clean > annotation

strictness. If a freshly-added annotation breaks a test, narrow it one rung in

the discipline ladder (e.g., concrete → object, or remove an Any widening

that broke a downstream isinstance check) before reverting the file.

-

Backward-compat check. Run iff

grep -l '@compatibility(is_backward_compatible=True)' <target> returns the

file — the decorator is the actual precondition for the golden file. The

broader "imports torch.fx" heuristic catches half of torch/.

python -m pytest test/test_fx.py::TestFXAPIBackwardCompatibility -x -v

-

Unit tests for the modified module. Search both ways before concluding

no coverage exists:

# 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/

If both come up empty, tell the user — don't silently skip. Type changes can

introduce real runtime regressions (Optional[X] vs X, Sequence vs

list when .append is called, etc.).

Notes

  • Forward refs in class bodies without from __future__ import annotations

still need string quoting:

class MyClass:

    def __new__(cls) -> "MyClass": ...
  • Committing: don't commit unless the user explicitly asks (per repo

CLAUDE.md). Stop and surface the diff for review when the file is clean.

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card