How to add type annotations to a Python project with pyrefly infer
Pyrefly can turn def total(numbers): into def total(numbers: list[int]) -> int: without running your code. The command is pyrefly infer, previously called autotype, and it writes draft annotations by reading call sites with static analysis.
For a legacy codebase with thousands of unannotated functions, this turns weeks of manual annotation into a reviewable diff. The output needs a human review pass before merging.
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 pyreflyYou can also run it ad-hoc without installing:
uvx pyrefly infer path/to/file.pyRun 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.pyThe command edits the file in place. For example, an unannotated function:
def total(numbers):
result = []
for n in numbers:
result.append(n * 2)
return sum(result)
if __name__ == "__main__":
print(total([1, 2, 3]))becomes:
def total(numbers: list[int]) -> int:
result = []
for n in numbers:
result.append(n * 2)
return sum(result)
if __name__ == "__main__":
print(total([1, 2, 3]))Pyrefly inferred numbers: list[int] from the call site total([1, 2, 3]) and -> int from sum().
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/Return types are the safest category to add first because they never change how the function is called. Parameter types can surface call-site mismatches that need a wider review.
Review the diff before checking
Commit existing work before running pyrefly infer, then review the diff:
git diffTwo 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]getslist[int]even if it should acceptIterable[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 checkA frequent pattern: Pyrefly infers a parameter type from one call site, but other call sites pass a different type. For example, given:
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
Noneorbool, 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.