Skip to content

How to add type annotations to a Python project with pyrefly infer

Pyrefly writes draft parameter and return annotations without running the project code. The command is pyrefly infer, and it writes draft annotations by reading call sites with static analysis.

Review the generated annotations before merging; inference can be incomplete or narrower than the intended API.

Note

pyrefly infer is under active development. The Pyrefly maintainers explicitly recommend manually reviewing every change it produces.

Install Pyrefly

Add Pyrefly as a dev dependency in a uv project:

uv add --dev pyrefly
uv run pyrefly init --non-interactive

The --dev flag marks Pyrefly as a development dependencyA package needed during development (testing, linting, formatting) that is not shipped to users of your project. Installed with the --dev flag. , keeping it out of production installs. uv run executes the command inside the project’s virtual environment, ensuring all dependencies are installed first. Initialization writes a native Pyrefly configuration; review migrated settings and select preset = "default" for the examples here.

With that configuration in place, it can also run without adding a project dependency:

uvx pyrefly infer path/to/file.py

Run pyrefly infer on a single file

Start with one file so you can review the diff before committing:

uv run pyrefly infer path/to/file.py

The command edits the file in place. For example, an unannotated function:

example.py
def total(numbers):
    result = []
    for n in numbers:
        result.append(n * 2)
    return sum(result)


if __name__ == "__main__":
    print(total([1, 2, 3]))

becomes:

example.py
def total(numbers: list[int]):
    result = []
    for n in numbers:
        result.append(n * 2)
    return sum(result)


if __name__ == "__main__":
    print(total([1, 2, 3]))

Pyrefly infers numbers: list[int] from the call site. The return annotation remains absent in this example; review incomplete output and add the intended -> int manually after checking the function body.

Choose what to annotate with flags

pyrefly infer exposes four flags that toggle each annotation category. Each accepts =true or =false:

Flag What it adds
--parameter-types Annotations on function parameters
--return-types Function return annotations
--containers Element types for list, dict, and other containers
--imports Imports for symbols introduced by new annotations

For an incremental rollout, run return types first and parameters later:

uv run pyrefly infer --return-types --parameter-types=false --containers=false src/

Both parameter and return annotations can expose errors at call sites. A return-only pass limits the diff, but does not guarantee that callers remain type-correct.

Review the diff before checking

Commit existing work before running pyrefly infer, then review the diff:

git diff

Two things to watch for:

  • Functions whose call sites are not visible to Pyrefly stay unannotated. Parameter inference depends on observing how the function is called inside the project. Internal helpers usually annotate cleanly; functions called only from tests, fixtures, or external consumers may not.
  • Pyrefly sometimes infers narrower types than you intend. A function called only with list[int] gets list[int] even if it should accept Iterable[int]. Widen these annotations manually before merging.

When the diff looks right, commit it:

git commit -am "Add type annotations with pyrefly infer"

Handle the new errors

New annotations turn previously-implicit Any into concrete types, which can surface latent inconsistencies. Run a check next:

uv run pyrefly check

A frequent pattern: Pyrefly infers a parameter type from one call site, but other call sites pass a different type. For example, given:

labels.py
def label(value):
    return f"label-{value}"


def process(items):
    return [label(item) for item in items]


if __name__ == "__main__":
    process([1, 2, 3])
    label("oops")

pyrefly infer writes def label(value: str) -> str because the direct call uses a string. After annotation, the indirect call from process fails:

$ uv run pyrefly check labels.py
ERROR Argument `int` is not assignable to parameter `value` with type `str` in function `label` [bad-argument-type]
 --> labels.py:8:32
  |
8 |     return [label(item) for item in items]
  |                ^^^^
  |
 INFO 1 error

Fix these errors by widening the annotation manually (value: str | int) or by suppressing the diagnostic with # pyrefly: ignore[bad-argument-type] while you investigate. The Pyrefly docs cover the error suppression syntax in detail.

Roll out incrementally

Run on small slices, not the whole project at once:

uv run pyrefly infer src/myproject/api/

After each batch, review and commit the diff before checking. Then run uv run pyrefly check, address the new errors, and move to the next directory. Smaller batches keep code review tractable and isolate the source of any regressions.

Compare with other auto-annotation tools

pyrefly infer uses static analysis only, so it never executes your code. Two alternatives take different approaches:

  • MonkeyType records types at runtime by tracing your test suite, then writes them back. It catches dynamic patterns static analysis misses, at the cost of running instrumented Python.
  • autotyping focuses on cheap-to-detect cases (returning None or bool, simple property setters) and is fast to apply across very large codebases.

MonkeyType can annotate dynamic paths that pyrefly infer will miss, but only for code your tests execute. pyrefly infer works without running the test suite, but it only sees what static analysis can prove from local call sites.

Last updated on